Compare commits
627 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb851b1738 | |||
| 82825a5b35 | |||
| a63f2d2a33 | |||
| d3983a603f | |||
| 9304319f55 | |||
| e778449377 | |||
| 3ecafbe0f3 | |||
| 9ab345b6dc | |||
| ade1a6b670 | |||
| af840d7169 | |||
| f2ced3f0e4 | |||
| eb2e151520 | |||
| b235c8591e | |||
| 4c11f00176 | |||
| f75d1e1afc | |||
| 4f419da1dc | |||
| d9c25ac4ce | |||
| 8dfedf2a5a | |||
| 19f94e1e6a | |||
| fc269ed78d | |||
| cfc633202b | |||
| 6f1790deef | |||
| 331ce14170 | |||
| a4c6cd1cd0 | |||
| cb0e336812 | |||
| ba7c9cef60 | |||
| 70b30ae431 | |||
| 12c267a5d1 | |||
| 38625a8cd3 | |||
| 700cd8c3e8 | |||
| c272fbe0ae | |||
| f40005da36 | |||
| 34afac0165 | |||
| 7cbe9a477b | |||
| 081d472176 | |||
| d7f8ebec39 | |||
| bde65e2066 | |||
| 4896cd208e | |||
| 42a5b3cd67 | |||
| d60fd2516f | |||
| 6b2c27cf45 | |||
| 1a3f4f64e5 | |||
| 492cbb4644 | |||
| c8e93b15db | |||
| e3f4fe3e13 | |||
| 8cb973cf3c | |||
| 22d1a72752 | |||
| 8f90331229 | |||
| 9a638449d5 | |||
| 9ce9b714f6 | |||
| 22b7aba035 | |||
| 4cbcf282f7 | |||
| d8b1ddfb59 | |||
| 7905cb379b | |||
| 1b2463d52e | |||
| b567f9957a | |||
| c2fb8c786d | |||
| 4320d8e1fa | |||
| 344d14dafc | |||
| 3cb894c4e5 | |||
| dbbe14ddd6 | |||
| 9ee07f431a | |||
| d517e9a0b9 | |||
| d9adbb6f09 | |||
| 6b77277cea | |||
| bad24392fc | |||
| f24e62129b | |||
| f38cb855ea | |||
| 891a0d50a6 | |||
| cfb384e618 | |||
| 6f14aa1c71 | |||
| f771e6a122 | |||
| a0a23947b6 | |||
| 7b6f1dc88b | |||
| 16f9e1705f | |||
| 40e28b702d | |||
| 75f0b843f8 | |||
| 8793385325 | |||
| 4f86143853 | |||
| 498f74b21b | |||
| 300798eb11 | |||
| d482fe6832 | |||
| 955bb20914 | |||
| 22339d261b | |||
| 5d89a40c6d | |||
| c1fe602c8b | |||
| bea5e0da1e | |||
| 7907e56a31 | |||
| 4e43e6a315 | |||
| 78ef08390e | |||
| b5235ed4f0 | |||
| 868a567ebd | |||
| 0797041643 | |||
| 60b22be29d | |||
| 1a817e1367 | |||
| 2be3044f6a | |||
| 59f6f4b926 | |||
| 3d2d36954e | |||
| e44cb3950f | |||
| 06682a6edc | |||
| e6e513a34b | |||
| 13dd3742a8 | |||
| c27278ecfb | |||
| 2f68c6e344 | |||
| a7976d2e63 | |||
| c15167ca86 | |||
| e75e534882 | |||
| b21ec34a65 | |||
| e45b1fb8df | |||
| 0b3c3ab39d | |||
| 8b530bc1c9 | |||
| a2212f7a89 | |||
| e613b54d94 | |||
| d9c140114a | |||
| 9b59f38c0d | |||
| 2a8ec0d786 | |||
| e29fad16ea | |||
| 50d223a30a | |||
| 1dbdf4ea9e | |||
| 09b5a19577 | |||
| b5ea72e214 | |||
| 57e3748d79 | |||
| 155626fbed | |||
| e802aa211f | |||
| 54fe8c6687 | |||
| 5da6e4ff75 | |||
| 4090bb100e | |||
| 54ed99d91d | |||
| 3db3eac62a | |||
| 0315882fa4 | |||
| 9757e2634f | |||
| 42b55f66fb | |||
| e64058bc94 | |||
| 386503cce0 | |||
| 6224ff49f6 | |||
| 6578be00ce | |||
| 2f325d898f | |||
| b039c599c8 | |||
| bec4b73839 | |||
| e67bac0868 | |||
| 24ff92068d | |||
| 6e20036d6c | |||
| b6059cfe4a | |||
| 4a305a2726 | |||
| 4ae6d8135f | |||
| 225a94199c | |||
| 35ffa00bae | |||
| 58e11d3c06 | |||
| fddd7c14ee | |||
| 7d755f215f | |||
| 7e473375e5 | |||
| 08b9ea6e47 | |||
| 245aea4ce8 | |||
| e1e41c20ea | |||
| e16052cdc9 | |||
| de2b38217a | |||
| b2a099689f | |||
| 42c8a02ef9 | |||
| 32058027f3 | |||
| 99fc685e14 | |||
| 414117b9e6 | |||
| 799bd92b28 | |||
| d7cc8dc229 | |||
| f4fa36a0ab | |||
| 4d5eb28cb4 | |||
| a008426317 | |||
| 2a212790b6 | |||
| 09f27a902a | |||
| 6cbbca9e92 | |||
| f1fd8fc38b | |||
| a67e1b81e1 | |||
| 936c5f7fda | |||
| ab3151f924 | |||
| d4fb318381 | |||
| 31a7b5892e | |||
| c469f0e85a | |||
| 355d36a838 | |||
| 43e46477df | |||
| 34a235fc68 | |||
| ce9f9af980 | |||
| 5f799ca23e | |||
| 0159b4315e | |||
| 864d7a0bfd | |||
| fa698266b3 | |||
| ba96bbd8ff | |||
| 20d5e3310a | |||
| 70fdb04db5 | |||
| 65c3c7eba9 | |||
| 98e218042d | |||
| 8a56f73c7c | |||
| ffdd15e275 | |||
| 8f71e174cb | |||
| 6621aaf877 | |||
| c528361d07 | |||
| 5376cfd6a9 | |||
| 76615b9262 | |||
| ce551fbdb0 | |||
| 108e790cf8 | |||
| 0b5424cadb | |||
| 48571f5032 | |||
| c6c9f65040 | |||
| ace6c20164 | |||
| d64b64f39e | |||
| 41a687a63e | |||
| f4f6704510 | |||
| 65fc0fadb3 | |||
| 64b686713b | |||
| 9c6d3e37d4 | |||
| 37abe9db39 | |||
| 9ebef4a268 | |||
| e60f7c7e77 | |||
| 36aadd0604 | |||
| e4598a5e37 | |||
| db5c22809c | |||
| c61fa8f858 | |||
| 5fe4681620 | |||
| 80d384361b | |||
| a1e72c0749 | |||
| e3a80b8247 | |||
| c0013acadc | |||
| f472c53da7 | |||
| 032f7931e8 | |||
| 57a12c1711 | |||
| d13ca3983f | |||
| 9769ce4ac9 | |||
| 55dc263dc9 | |||
| e56ae23747 | |||
| bfd4af5d69 | |||
| 9673604878 | |||
| ae55aecb0d | |||
| a14d0b4f2f | |||
| 301e4a17da | |||
| 19cebbf673 | |||
| ef73c07dfe | |||
| ca5ad7e51b | |||
| 5dab944abc | |||
| adaae6e1a4 | |||
| 4524cbdf76 | |||
| 1171cf52f0 | |||
| c5978b1072 | |||
| e5ef9367ed | |||
| 0ca908a6cc | |||
| e20c012eb3 | |||
| e88df26d7c | |||
| 9491e6845e | |||
| e4433c4212 | |||
| c6fdc5a5d5 | |||
| 68dd3fc027 | |||
| cd7aec6752 | |||
| b80556498d | |||
| 38530cdb39 | |||
| 078fd5990e | |||
| b4be5feaa6 | |||
| d982f97bd8 | |||
| 653608c98c | |||
| d63be2d6f3 | |||
| 4500ca577f | |||
| cc935daec0 | |||
| 51ff46ec58 | |||
| 206b7731cf | |||
| d16f59d33d | |||
| a5ec046346 | |||
| 51dd803115 | |||
| 6e39424a30 | |||
| 6ca84782aa | |||
| ee1ba6369e | |||
| f35d70e6fe | |||
| 430a3acfeb | |||
| 395c5841f9 | |||
| 173161ca4d | |||
| a1a8e14368 | |||
| 2379806c61 | |||
| f604208a8b | |||
| cf31fbec5d | |||
| 279be8deb7 | |||
| 7366d8aab7 | |||
| a7ab409d48 | |||
| 9dea0cf3ae | |||
| 50d7c1ec80 | |||
| f32cdafdf6 | |||
| 2e2272d07a | |||
| 27ffff7e0a | |||
| 305cebe0d0 | |||
| 4602a068f8 | |||
| 3a947d72c6 | |||
| 1940e3530f | |||
| dd549dc686 | |||
| 3417bd3ed2 | |||
| 9470d2b696 | |||
| f523a7d31d | |||
| ea4c2b035b | |||
| c5bfba837f | |||
| c5ad957b25 | |||
| cc849e7cea | |||
| 22428da54d | |||
| f6071f7e2d | |||
| 08e8560865 | |||
| 75f01db543 | |||
| 5a028d2551 | |||
| 07a4ba8bc5 | |||
| f84921f735 | |||
| 872494e490 | |||
| d0848b0be5 | |||
| c848956777 | |||
| 296997d10e | |||
| ffdb22238c | |||
| a63c320403 | |||
| aa86ed78a3 | |||
| 19456ec855 | |||
| 2c57840c44 | |||
| 26b7927f62 | |||
| d4267af725 | |||
| 16a2aedcd0 | |||
| 755f04f777 | |||
| cf9539ddab | |||
| 99d558f33c | |||
| 1fe073004b | |||
| 6ae5fb5edb | |||
| 3a3953edf6 | |||
| c8e788eaf8 | |||
| 088a44496d | |||
| 60a57c5abc | |||
| 482466bbbf | |||
| 65deb7317b | |||
| 00ae061822 | |||
| 9bd7fc1bd5 | |||
| 98ddb83f1d | |||
| 6dd8b9d072 | |||
| 1e3482a878 | |||
| 734846843f | |||
| f1eac66494 | |||
| b4c3280f58 | |||
| b2f2998767 | |||
| 720eb93c67 | |||
| cdf8611e0b | |||
| 905d797b65 | |||
| 6ca8ccc332 | |||
| 76496e8865 | |||
| f38fec8588 | |||
| 4351ac9657 | |||
| f3834ac0e2 | |||
| 48891a7ca5 | |||
| 3b6a4aa635 | |||
| b20f0471c2 | |||
| 0f9e66e1e8 | |||
| 6fb4cee43f | |||
| 0e8afbbf56 | |||
| 83decf7bf5 | |||
| 5c77cf23b8 | |||
| 606fd3164c | |||
| 71308d0d6f | |||
| 8bbea7cda4 | |||
| a87d3e4809 | |||
| 1f3db58254 | |||
| 43f6c06271 | |||
| 04ae542b13 | |||
| f97c81df0d | |||
| b7b3fc53eb | |||
| 4b44fb849d | |||
| 5d0a266e9c | |||
| 24667861db | |||
| 25cc403886 | |||
| 923312c482 | |||
| adeef6eb73 | |||
| 6f84d45960 | |||
| 938c25cab4 | |||
| 0404637da8 | |||
| 98d95e4b7f | |||
| 2687a35781 | |||
| 886dae28fa | |||
| 8c9bc8cdbc | |||
| 5414a0aa34 | |||
| 223cf8613b | |||
| 4f1776befc | |||
| 38473b9f6c | |||
| 41b428c75f | |||
| d8d754658f | |||
| e52ebf1d53 | |||
| 535c7a5f92 | |||
| 99b2bd784a | |||
| 31ebf966fd | |||
| 6036c6e8e0 | |||
| 597151b5dd | |||
| 29aeaed223 | |||
| edf87952c7 | |||
| 78f8434a06 | |||
| 7eaccad683 | |||
| bfa9c1e1c8 | |||
| a2d6577579 | |||
| 0e8191488f | |||
| a1510bb081 | |||
| 1eaca30f97 | |||
| bc79987ed5 | |||
| 36a501506e | |||
| e1d47f5f63 | |||
| 7faa9b5a64 | |||
| 295bee708c | |||
| 9e917481c4 | |||
| c5a8b9fb3d | |||
| 3f4e32f8da | |||
| f630098f1d | |||
| 608d6e2b54 | |||
| fad5ab6c44 | |||
| d4cb7997fe | |||
| 28364b8be1 | |||
| d28a5c7d5e | |||
| df707aef4c | |||
| 77af2e58ec | |||
| f738e56337 | |||
| f2fe2584e0 | |||
| 02673ae017 | |||
| 8dd294a163 | |||
| 469e735d53 | |||
| b02001796e | |||
| 0d4d2fdd7e | |||
| b65e5446fa | |||
| f75fe3ce5b | |||
| 59ba2712cf | |||
| fefe601541 | |||
| 7291c60413 | |||
| bb679102b1 | |||
| 16027c8971 | |||
| 84405a33e3 | |||
| bdf795c3f2 | |||
| ab73084bb5 | |||
| e9fb4c5414 | |||
| bc54f525e4 | |||
| 9170159bc1 | |||
| fa657a0366 | |||
| 24920ddd52 | |||
| eb0b372113 | |||
| 8f5f4f674e | |||
| 38c1dae9c7 | |||
| f92bcd1d14 | |||
| df09b86d23 | |||
| bd5cfa10ad | |||
| 94914dd1ad | |||
| dfbb58a2f1 | |||
| 54a1c3576f | |||
| 9ddf73b4fe | |||
| 4ceec1a6ab | |||
| ba314d7434 | |||
| 55fed67067 | |||
| 852390092e | |||
| 067be3fed3 | |||
| 1aecdbb71c | |||
| d8e517cb7b | |||
| 9da5b4cacc | |||
| 84af32242b | |||
| 7a81472407 | |||
| 9399d8df42 | |||
| 459f73fd5c | |||
| c1c3d3b7c5 | |||
| 380a9630e5 | |||
| 984568e00c | |||
| 5fefd8e083 | |||
| a6eef643f6 | |||
| a01928e010 | |||
| d901db2316 | |||
| fdda812078 | |||
| 7c59e9d5b5 | |||
| c9b0c4da34 | |||
| 9f214141b4 | |||
| 4638e90f02 | |||
| d1c50ab6f8 | |||
| 489aa0ffd8 | |||
| ebcc605961 | |||
| bb630049d5 | |||
| 39321adfe1 | |||
| ea2b8df906 | |||
| 74a9b1aef4 | |||
| 8b3e35de94 | |||
| 9d4724fa99 | |||
| a8379cbd80 | |||
| bdfb6c5a84 | |||
| 87414d7137 | |||
| f9d0b64d7a | |||
| 97918710bd | |||
| ac340b3f1e | |||
| a2097e148b | |||
| 190be7f732 | |||
| 0333ce5c46 | |||
| 47df623fd4 | |||
| c609efbd9a | |||
| c8cd41a833 | |||
| 02a0110091 | |||
| 6c572c56a3 | |||
| 2db516ef3b | |||
| 9f3787cd95 | |||
| 5511943237 | |||
| 626838eba6 | |||
| 587328f54c | |||
| 1aa88b1fb6 | |||
| e7db119545 | |||
| da79536456 | |||
| aaaff43731 | |||
| 5d77142758 | |||
| 586ea0002b | |||
| e642d6fe76 | |||
| 7148f288d5 | |||
| 40abf3fb5b | |||
| adea6c7177 | |||
| 96a307e99f | |||
| f0201f72ef | |||
| 0b1b677b3d | |||
| b1dcec86cd | |||
| 7c783fe565 | |||
| 9a0207d7cf | |||
| 1b0e047d6b | |||
| 7ab3c7e5d9 | |||
| 9ece49d226 | |||
| 1272e2ec74 | |||
| db16602018 | |||
| 5c202805b9 | |||
| bfc8866b69 | |||
| 9aedc29289 | |||
| e1f3d75ac4 | |||
| 030f23d2e9 | |||
| 6be9a4e71b | |||
| eb0bffb5dd | |||
| 240bbb3676 | |||
| 7d25110a6e | |||
| 598eea703b | |||
| 8793f7bff4 | |||
| c1b3911622 | |||
| b0bf05109d | |||
| d9cb0ebcc8 | |||
| 1eda21f63f | |||
| f2dbcde124 | |||
| 60f00db2c5 | |||
| a9332453fc | |||
| b99da14799 | |||
| f02400a1fb | |||
| bbcb7f7cdd | |||
| 30cf8e6d54 | |||
| 4f772fc0f7 | |||
| 2cf2582138 | |||
| ad4de4b92c | |||
| 27ed73a6ae | |||
| a1570c8917 | |||
| 7e419ddd43 | |||
| b045d3616d | |||
| f6b50e2e77 | |||
| 9d1381cbb3 | |||
| 99b0242d86 | |||
| db27628211 | |||
| 9b9d86229a | |||
| 46f830dacf | |||
| 3dd9bc7772 | |||
| c7335add19 | |||
| c133498208 | |||
| 6a52bb22a3 | |||
| 6f970efda5 | |||
| c69350a0eb | |||
| 4add8820eb | |||
| 033dd4811e | |||
| da5cd32502 | |||
| edf33fdec9 | |||
| 448cbf16c9 | |||
| e45b77b528 | |||
| b61fd1716e | |||
| d584b5cc7e | |||
| faf9cedfbd | |||
| 3a38ac9339 | |||
| 7ea36a3cfc | |||
| 3577c0b47a | |||
| d0bef42186 | |||
| d9f10c62ad | |||
| 35d3c03e4b | |||
| fdd38a1580 | |||
| 3018801f06 | |||
| 8e96bfef06 | |||
| 31071bd11e | |||
| f5619f6be1 | |||
| adad1b5a2c | |||
| a6c4e19a5d | |||
| af6f74c344 | |||
| ce45cb8a89 | |||
| 14d4aa202d | |||
| 4c35798dba | |||
| 656a9b365f | |||
| 67fa3db921 | |||
| f2d3061e97 | |||
| 760c34e416 | |||
| 48398b315c | |||
| 45d0d9d7f1 | |||
| b0f4ac8221 | |||
| 719192b1aa | |||
| 7eb32bdac8 | |||
| 862756fd5b | |||
| 5ba16e72e0 | |||
| e43ae0504d | |||
| acb6cd180c | |||
| a3301d0413 | |||
| b9e72358ac | |||
| 61b42ef326 | |||
| da9e27c28d | |||
| 6f80fbabaa | |||
| d2308bd45c | |||
| d35972d24f | |||
| 298a264934 | |||
| ad63b595b4 | |||
| 986a9ceebf | |||
| 682b65e723 | |||
| cb661506c0 | |||
| 505aa06443 | |||
| f534a978f1 | |||
| 1293815934 | |||
| 0c0fd18bf4 | |||
| c49c31813f | |||
| 877ab11bb4 | |||
| 0d18d8d7e3 | |||
| 5f977857a1 | |||
| a63b21d417 | |||
| e1b7d6090e | |||
| 1bcc5cdd81 | |||
| 873fdbc4b3 | |||
| 79128effea | |||
| 14ef94fc09 | |||
| 759e3e3a4d | |||
| 5b7b16efba | |||
| 7fd171acb5 | |||
| 02ed3f1d44 | |||
| 4f7f3f36a3 | |||
| 73a8cf12a9 | |||
| 5f5eb16705 | |||
| f0f4437870 |
@@ -7,6 +7,7 @@
|
||||
pnpm-lock.yaml linguist-generated
|
||||
package-lock.json linguist-generated
|
||||
bun.lockb linguist-generated
|
||||
bun.lock linguist-generated
|
||||
yarn.lock linguist-generated
|
||||
|
||||
# Exclude templates from language statistics
|
||||
|
||||
+4
-1
@@ -1,5 +1,5 @@
|
||||
# Set default
|
||||
* @aklinker1 @Timeraa
|
||||
* @aklinker1
|
||||
|
||||
# Secure Directories
|
||||
/.github/ @aklinker1
|
||||
@@ -7,3 +7,6 @@
|
||||
# Creator of specific wxt modules
|
||||
/packages/auto-icons/ @Timeraa
|
||||
/packages/unocss/ @Timeraa
|
||||
|
||||
# Other Directories
|
||||
/docs @aklinker1 @PatrykKuniczak @nishu-murmu
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
name: Basic Setup
|
||||
description: Install PNPM, Node, and dependencies
|
||||
description: Install Bun and dependencies
|
||||
|
||||
inputs:
|
||||
install:
|
||||
default: 'true'
|
||||
type: boolean
|
||||
description: Whether or not to run 'pnpm install'
|
||||
|
||||
installArgs:
|
||||
default: ''
|
||||
type: string
|
||||
description: Additional args to append to "pnpm install"
|
||||
description: Whether or not to run 'bun install'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: 🛠️ Setup NodeJS
|
||||
uses: actions/setup-node@v4
|
||||
- name: 🛠️ Setup Bun
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
||||
with:
|
||||
node-version: 18
|
||||
cache: pnpm
|
||||
bun-version-file: '.tool-versions'
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
if: ${{ inputs.install == 'true' }}
|
||||
shell: bash
|
||||
run: pnpm install ${{ inputs.installArgs }}
|
||||
run: bun ci ${{ inputs.installArgs }}
|
||||
|
||||
+12
-7
@@ -1,15 +1,20 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
- package-ecosystem: bun
|
||||
directory: /
|
||||
schedule:
|
||||
interval: 'monthly'
|
||||
interval: 'daily'
|
||||
groups:
|
||||
production-updates:
|
||||
applies-to: version-updates
|
||||
dependency-type: production
|
||||
update-types: [minor, patch]
|
||||
dev-updates:
|
||||
applies-to: version-updates
|
||||
dependency-type: development
|
||||
update-types: [minor, patch]
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'monthly'
|
||||
interval: 'daily'
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
name: ✨ Auto-label PR
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronized, reopened]
|
||||
|
||||
jobs:
|
||||
update-pr:
|
||||
name: Update PR
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Gather Info
|
||||
id: check
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number
|
||||
});
|
||||
|
||||
// Check if PR has assignees
|
||||
const hasAssignees = pr.assignees && pr.assignees.length > 0;
|
||||
core.setOutput('has_assignees', hasAssignees);
|
||||
core.setOutput('author', pr.user.login);
|
||||
|
||||
// Get list of changed files
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number
|
||||
});
|
||||
|
||||
// Find all packages that were modified
|
||||
const packagesRegex = /^packages\/([^\/]+)\//;
|
||||
const affectedPackages = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const match = file.filename.match(packagesRegex);
|
||||
if (match) {
|
||||
affectedPackages.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const labels = Array.from(affectedPackages).map(pkg => `pkg/${pkg}`);
|
||||
core.setOutput('labels', JSON.stringify(labels));
|
||||
console.log('Detected package labels:', labels);
|
||||
|
||||
// Get current labels on the PR that match pkg/* pattern
|
||||
const currentPkgLabels = pr.labels
|
||||
.map(label => label.name)
|
||||
.filter(name => name.startsWith('pkg/'));
|
||||
|
||||
core.setOutput('current_pkg_labels', JSON.stringify(currentPkgLabels));
|
||||
console.log('Current pkg labels:', currentPkgLabels);
|
||||
|
||||
- name: Sync Author
|
||||
if: steps.check.outputs.has_assignees == 'false'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
assignees: ['${{ steps.check.outputs.author }}']
|
||||
});
|
||||
|
||||
console.log('Assigned PR author: ${{ steps.check.outputs.author }}');
|
||||
} catch (err) {
|
||||
console.warn("Error assigning author:", err);
|
||||
}
|
||||
|
||||
- name: Sync Labels
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const newLabels = ${{ steps.check.outputs.labels }};
|
||||
const currentLabels = ${{ steps.check.outputs.current_pkg_labels }};
|
||||
|
||||
// Find labels to add (in newLabels but not in currentLabels)
|
||||
const labelsToAdd = newLabels.filter(label => !currentLabels.includes(label));
|
||||
|
||||
// Find labels to remove (in currentLabels but not in newLabels)
|
||||
const labelsToRemove = currentLabels.filter(label => !newLabels.includes(label));
|
||||
|
||||
// Add new labels
|
||||
if (labelsToAdd.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: labelsToAdd
|
||||
});
|
||||
console.log('Added labels:', labelsToAdd);
|
||||
}
|
||||
|
||||
// Remove obsolete labels
|
||||
for (const label of labelsToRemove) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
name: label
|
||||
});
|
||||
console.log('Removed label:', label);
|
||||
}
|
||||
|
||||
if (labelsToAdd.length === 0 && labelsToRemove.length === 0) {
|
||||
console.log('No label changes needed');
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
name: 🔔 Notify Unreleased Commits
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 20 * * 1' # Weekly at 8 PM UTC (3 PM CT)
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.repository_owner == 'wxt-dev'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: List Commits
|
||||
id: list-commits
|
||||
run: |
|
||||
./scripts/list-unreleased-commits.sh > summary.txt
|
||||
|
||||
- name: Discord notification
|
||||
run: |
|
||||
PAYLOAD=$(jq -n --arg content "${{ env.MESSAGE }}" '{"content": $content}')
|
||||
curl -X POST \
|
||||
-F "payload_json=${PAYLOAD}" \
|
||||
-F "file1=@summary.txt" \
|
||||
$DISCORD_WEBHOOK
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_UNRELEASED_COMMITS }}
|
||||
MESSAGE: |
|
||||
If a package needs released, please [run the workflow](<https://github.com/wxt-dev/wxt/actions/workflows/release.yml>).
|
||||
|
||||
Before running, consider:
|
||||
- Are there any breaking changes? If so, prepare a list of breaking changes and update the changelog and release notes after the release.
|
||||
- Are there any PRs open that we wait to release after they're merged?
|
||||
@@ -17,13 +17,18 @@ jobs:
|
||||
if: ${{ github.repository == 'wxt-dev/wxt' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build All Packages
|
||||
run: pnpm buildc all
|
||||
run: bun run buildc all
|
||||
|
||||
- name: Publish
|
||||
run: pnpx pkg-pr-new publish --compact --pnpm './packages/*'
|
||||
run: bunx pkg-pr-new publish --compact --bun './packages/*'
|
||||
|
||||
- name: DEBUG
|
||||
if: always()
|
||||
run: ls -lhA
|
||||
working-directory: packages/analytics
|
||||
|
||||
@@ -15,8 +15,21 @@ jobs:
|
||||
if: github.event.pull_request.merged == true
|
||||
|
||||
steps:
|
||||
- name: Log PR Author Info
|
||||
run: |
|
||||
echo "PR Author Login: ${{ github.event.pull_request.user.login }}"
|
||||
echo "PR Author ID: ${{ github.event.pull_request.user.id }}"
|
||||
echo "PR Author Type: ${{ github.event.pull_request.user.type }}"
|
||||
echo "PR Author Node ID: ${{ github.event.pull_request.user.node_id }}"
|
||||
|
||||
- name: Post Thank You Comment
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
if: >-
|
||||
github.event.pull_request.user.login != 'aklinker1' &&
|
||||
github.event.pull_request.user.login != 'Timeraa' &&
|
||||
github.event.pull_request.user.login != 'PatrykKuniczak' &&
|
||||
github.event.pull_request.user.login != 'dependabot[bot]' &&
|
||||
github.event.pull_request.user.login != 'copilot-swe-agent[bot]'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
comment: Thanks for helping make WXT better!
|
||||
with:
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
# Only fetch the config file from the repository
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
@@ -23,13 +23,13 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: https://${{ secrets.DOCKER_REGISTRY_HOSTNAME }}
|
||||
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
||||
@@ -37,11 +37,11 @@ jobs:
|
||||
|
||||
- name: Build docs
|
||||
run: |
|
||||
pnpm docs:build
|
||||
bun run docs:build
|
||||
docker build docs/.vitepress -t ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
|
||||
|
||||
- name: Push Image
|
||||
run: docker push ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
|
||||
|
||||
- name: Deploy
|
||||
run: curl -X POST -i ${{ secrets.UPDATE_DOCS_WEBHOOK }}
|
||||
run: 'curl -X POST -i -H "X-API-Key: ${{ secrets.DOCS_WEBHOOK_API_KEY }}" https://${{ secrets.DOCS_WEBHOOK_DOMAIN }}/api/webhook/stacks/${{ secrets.DOCS_WEBHOOK_STACK_ID }}'
|
||||
|
||||
@@ -10,10 +10,12 @@ on:
|
||||
- analytics
|
||||
- auto-icons
|
||||
- i18n
|
||||
- is-background
|
||||
- module-react
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- runner
|
||||
- storage
|
||||
- unocss
|
||||
- webextension-polyfill
|
||||
@@ -30,16 +32,18 @@ jobs:
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
contents: write # Push version changes
|
||||
id-token: write # OIDC for NPM publishing
|
||||
needs:
|
||||
- validate
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY }} # https://github.com/sbellone/release-workflow-example
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -52,18 +56,18 @@ jobs:
|
||||
|
||||
- name: Bump and Tag
|
||||
run: |
|
||||
pnpm tsx scripts/bump-package-version.ts ${{ inputs.package }}
|
||||
bun run scripts/bump-package-version.ts ${{ inputs.package }}
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
- name: Publish to NPM
|
||||
working-directory: packages/${{ inputs.package }}
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }}" > ~/.npmrc
|
||||
pnpm build
|
||||
pnpm publish
|
||||
bun pm pack
|
||||
sudo npm i -g npm@latest
|
||||
npm publish *.tgz
|
||||
|
||||
- name: Create GitHub release
|
||||
run: pnpm tsx scripts/create-github-release.ts ${{ inputs.package }}
|
||||
run: bun run scripts/create-github-release.ts ${{ inputs.package }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -10,10 +10,12 @@ on:
|
||||
- analytics
|
||||
- auto-icons
|
||||
- i18n
|
||||
- is-background
|
||||
- module-react
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- runner
|
||||
- storage
|
||||
- webextension-polyfill
|
||||
- wxt
|
||||
@@ -29,7 +31,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -37,6 +39,6 @@ jobs:
|
||||
installArgs: --ignore-scripts
|
||||
|
||||
- name: Sync Releases
|
||||
run: pnpm tsx scripts/sync-releases.ts ${{ inputs.package }}
|
||||
run: bun run scripts/sync-releases.ts ${{ inputs.package }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -11,11 +11,15 @@ jobs:
|
||||
sync:
|
||||
name: 'Sync with @types/chrome'
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'wxt-dev'
|
||||
permissions:
|
||||
contents: write
|
||||
contents: write # Push version changes
|
||||
id-token: write # OIDC for NPM publishing
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -24,24 +28,24 @@ jobs:
|
||||
|
||||
- name: Generate Latest Code
|
||||
working-directory: packages/browser
|
||||
run: pnpm gen
|
||||
run: bun run gen
|
||||
|
||||
- name: Run Checks
|
||||
working-directory: packages/browser
|
||||
run: pnpm check
|
||||
run: bun run check
|
||||
|
||||
- name: Commit Changes
|
||||
id: commit
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
commit_message: 'fix: Upgrade \`@wxt-dev/browser\` to latest \`@types/chrome\` version'
|
||||
file_pattern: 'packages/browser/**'
|
||||
commit_message: 'fix: Upgrade `@wxt-dev/browser` to latest `@types/chrome` version'
|
||||
|
||||
- name: Publish Package
|
||||
- name: Publish to NPM
|
||||
if: steps.commit.outputs.changes_detected == 'true'
|
||||
working-directory: packages/browser
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }}" > ~/.npmrc
|
||||
pnpm publish
|
||||
bun pm pack
|
||||
sudo npm i -g npm@latest
|
||||
npm publish *.tgz
|
||||
|
||||
@@ -15,79 +15,83 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Basic Checks
|
||||
run: pnpm check
|
||||
run: bun run check
|
||||
|
||||
builds:
|
||||
name: Builds
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build All Packages
|
||||
run: pnpm buildc all
|
||||
run: bun run buildc all
|
||||
|
||||
build-demo:
|
||||
name: Build Demo
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build
|
||||
run: pnpm build:all
|
||||
run: bun run build:all
|
||||
working-directory: packages/wxt-demo
|
||||
|
||||
- name: ZIP
|
||||
run: pnpm wxt zip
|
||||
run: bun run wxt zip
|
||||
working-directory: packages/wxt-demo
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-22.04
|
||||
name: Tests (${{ matrix.title }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- title: 'Linux'
|
||||
os: ubuntu-22.04
|
||||
coverage: true
|
||||
- title: 'Windows'
|
||||
os: windows-latest
|
||||
coverage: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Run Tests
|
||||
run: pnpm test:coverage -- --reporter=default --reporter=hanging-process
|
||||
if: ${{ ! matrix.coverage }}
|
||||
run: bun run test
|
||||
|
||||
- name: Run Tests (Coverage)
|
||||
if: matrix.coverage
|
||||
run: bun run test:coverage --reporter=default --reporter=hanging-process
|
||||
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
if: matrix.coverage
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
windows-tests:
|
||||
name: Windows Tests
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Run Tests
|
||||
run: pnpm test
|
||||
|
||||
template:
|
||||
name: Template
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -102,13 +106,13 @@ jobs:
|
||||
- vue
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Pack WXT package
|
||||
run: pnpm pack
|
||||
run: bun pm pack
|
||||
working-directory: packages/wxt
|
||||
|
||||
- name: Install Dependencies
|
||||
@@ -120,15 +124,15 @@ jobs:
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
|
||||
- name: Type Check Template
|
||||
run: pnpm compile
|
||||
run: bun run compile
|
||||
if: matrix.template != 'svelte'
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
|
||||
- name: Type Check Template
|
||||
run: pnpm check
|
||||
run: bun run check
|
||||
if: matrix.template == 'svelte'
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
|
||||
- name: Build Template
|
||||
run: pnpm build
|
||||
run: bun run build
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
|
||||
+19
-13
@@ -11,35 +11,41 @@ permissions:
|
||||
jobs:
|
||||
vhs:
|
||||
name: Create VHS
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: macos-latest
|
||||
if: ${{ github.repository == 'wxt-dev/wxt' }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
install: false
|
||||
|
||||
# This prevents pnpm dlx from downloading WXT in the video
|
||||
- name: Pre-install WXT
|
||||
run: |
|
||||
pnpm store add wxt@latest
|
||||
pnpm dlx wxt@latest --version
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
|
||||
with:
|
||||
go-version: '1.25.1'
|
||||
|
||||
# This prevents bunx from downloading WXT in the video, and it uses the checked-out version
|
||||
- name: Build WXT
|
||||
working-directory: packages/wxt
|
||||
run: bun run build
|
||||
|
||||
- name: Record VHS
|
||||
uses: charmbracelet/vhs-action@v2.1.0
|
||||
with:
|
||||
path: 'docs/tapes/init-demo.tape'
|
||||
run: |
|
||||
brew install ttyd ffmpeg
|
||||
go install github.com/charmbracelet/vhs@517bcda0faf416728bcf6b7fe489eb0e2469d9b5 # v0.10.0
|
||||
vhs docs/tapes/init-demo.tape
|
||||
|
||||
- name: Save recorded GIF
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
commit_message: 'docs: Update `wxt init` GIF'
|
||||
commit_user_name: github-actions[bot]
|
||||
commit_user_email: github-actions[bot]@users.noreply.github.com
|
||||
commit_author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
||||
# https://github.com/charmbracelet/vhs#output
|
||||
file_pattern: 'docs/assets/*.gif'
|
||||
|
||||
+4
-1
@@ -1,12 +1,16 @@
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!packages/wxt/src/core/utils/__tests__/fixtures/.env
|
||||
.idea
|
||||
.output
|
||||
.webextrc
|
||||
.wxt
|
||||
.wxt-runner
|
||||
*.log
|
||||
*.tgz
|
||||
/docs/.vitepress/cache
|
||||
docs/.vitepress/.temp
|
||||
coverage
|
||||
dist
|
||||
node_modules
|
||||
@@ -18,6 +22,5 @@ templates/*/yarn.lock
|
||||
templates/*/package-lock.json
|
||||
docs/api/reference
|
||||
stats.html
|
||||
.tool-versions
|
||||
.cache
|
||||
*-stats.txt
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/refs/heads/main/schema/markdownlint-config-schema.json",
|
||||
"line-length": false,
|
||||
"no-inline-html": false,
|
||||
"first-line-heading": false
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.git
|
||||
.output
|
||||
dist
|
||||
|
||||
# Generated files
|
||||
packages/wxt/README.md
|
||||
packages/*/CHANGELOG.md
|
||||
docs/api
|
||||
@@ -1,2 +1,4 @@
|
||||
singleQuote: true
|
||||
endOfLine: lf
|
||||
plugins:
|
||||
- prettier-plugin-jsdoc
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
bun 1.3.13
|
||||
nodejs 24.14.1
|
||||
Vendored
+2
-1
@@ -10,5 +10,6 @@
|
||||
// Additional guidelines for Copilot
|
||||
"github.copilot.chat.codeGeneration.instructions": [
|
||||
{ "file": "CONTRIBUTING.md" }
|
||||
]
|
||||
],
|
||||
"oxc.enable": true
|
||||
}
|
||||
|
||||
+4
-4
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
aaronklinker1@gmail.com.
|
||||
<aaronklinker1@gmail.com>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
@@ -116,7 +116,7 @@ the community.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
@@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity).
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
<https://www.contributor-covenant.org/faq>. Translations are available at
|
||||
<https://www.contributor-covenant.org/translations>.
|
||||
|
||||
+89
-37
@@ -6,6 +6,19 @@ If you are changing the docs or fixing a bug, feel free to fork and open a PR.
|
||||
|
||||
If you want to add a new feature, please create an issue or discussion first so we can decide if the feature is inline with the vision for WXT.
|
||||
|
||||
## WXT's Vision
|
||||
|
||||
WXT is two things:
|
||||
|
||||
1. A build tool
|
||||
2. A set of runtime utilities
|
||||
|
||||
The long term goal of WXT is provide an opinionated build tool that keeps WXT projects standard, while providing light-weight runtime utils that simplify a lot of the boilerplate/overhead when setting up a new extension.
|
||||
|
||||
I also want to provide a way for developers to use either one of those two things independently, and not require them to use both. This is why all of WXT's runtime utils are shipped as their own NPM packages, most of them not bundled inside the core `wxt` package. If you just want to use the packages, they're available, and if you just want to use WXT's built tool, you don't have to import any of WXT's utilities, you can use your own.
|
||||
|
||||
> The few runtime utils shipped inside WXT are things that should be used by 90% of extensions. That said, they're also legacy utils left in from before I started creating separate NPM packages, and in the future, they may be removed from the core package.
|
||||
|
||||
## Conventional Commits
|
||||
|
||||
This project uses [Conventional Commit format](https://www.conventionalcommits.org/en/v1.0.0/) to automatically generate a changelog and better understand the changes in the project
|
||||
@@ -20,18 +33,27 @@ Here are some examples of conventional commit messages:
|
||||
|
||||
The title of your pull request should follow the [conventional commit format](#conventional-commits). When a pull request is merged to the main branch, all changes are going to be squashed into a single commit. The message of this commit will be the title of the pull request. And for every release, the commit messages are used to generate the changelog.
|
||||
|
||||
## Breaking Changes Policy
|
||||
|
||||
A quick word on WXT's breaking changes policy. I am willing to make breaking changes, but they have to be for a good enough reason - they have to make WXT better as a whole, they can't be based on one opinion.
|
||||
|
||||
Breaking changes also require a major release. Major releases have happened once or twice a year, so after you merge your PR, you'll have to wait a little bit before it is released.
|
||||
|
||||
To make a breaking change:
|
||||
|
||||
1. Make sure you're PR is targeting the `major` branch
|
||||
2. Add `!` after the conventional commit type (`fix!: ...`, `feat!: ...`, `chore!: ...`, etc) to indicate that it is a breaking change
|
||||
3. At the top of the PR, provide documentation that will inform developers about the breaking change, why it was done, and how to migrate their extension so nothing breaks.
|
||||
- This documentation will be put ["Upgrading WXT"](https://wxt.dev/guide/resources/upgrading.html) page in the docs, read through previous breaking change docs for an idea of what is required.
|
||||
|
||||
## Setup
|
||||
|
||||
WXT uses `pnpm`, so make sure you have it installed.
|
||||
WXT uses Bun for package management and development. Install it from: <https://bun.com/>
|
||||
|
||||
Then install dependencies:
|
||||
|
||||
```sh
|
||||
corepack enable
|
||||
```
|
||||
|
||||
Then, simply run the install command:
|
||||
|
||||
```sh
|
||||
pnpm i
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development
|
||||
@@ -39,48 +61,68 @@ pnpm i
|
||||
Here are some helpful commands:
|
||||
|
||||
```sh
|
||||
# Build WXT package
|
||||
cd packages/wxt
|
||||
pnpm build
|
||||
# Build WXT package and workspace dependencies
|
||||
bun run --filter wxt build
|
||||
```
|
||||
|
||||
```sh
|
||||
# Build WXT package, then build demo extension
|
||||
cd packages/wxt-demo
|
||||
pnpm build
|
||||
```
|
||||
|
||||
```sh
|
||||
# Build WXT package, then start the demo extension in dev mode
|
||||
cd packages/wxt-demo
|
||||
pnpm dev
|
||||
# Build workspace dependencies, then start the demo extension in dev mode
|
||||
bun run --filter wxt-demo dev
|
||||
```
|
||||
|
||||
```sh
|
||||
# Run unit and E2E tests
|
||||
pnpm test
|
||||
bun run test
|
||||
```
|
||||
|
||||
```sh
|
||||
# Start the docs website locally
|
||||
pnpm docs:dev
|
||||
bun run docs:dev
|
||||
```
|
||||
|
||||
> Above, we used bun's `--filter` flag to choose which package to run a command in, but there are other ways:
|
||||
>
|
||||
> ```sh
|
||||
> bun run -F @wxt-dev/i18n build
|
||||
> # or
|
||||
> bun run --cwd packages/i18n build
|
||||
> # or
|
||||
> cd packages/i18n
|
||||
> bun run build
|
||||
> ```
|
||||
>
|
||||
> Pick your poison!
|
||||
|
||||
## Profiling
|
||||
|
||||
```sh
|
||||
# Build the latest version
|
||||
pnpm --filter wxt build
|
||||
bun run --filter wxt build
|
||||
|
||||
# CD to the demo directory
|
||||
cd packages/wxt-demo
|
||||
|
||||
# 1. Generate a flamechart with 0x
|
||||
pnpm dlx 0x node_modules/wxt/bin/wxt.mjs build
|
||||
# 2. Inspect the process with chrome @ chrome://inspect
|
||||
pnpm node --inspect node_modules/wxt/bin/wxt.mjs build
|
||||
```
|
||||
|
||||
Then there are a few different ways to profile WXT commands:
|
||||
|
||||
- Generate a flamechart with 0x:
|
||||
|
||||
```sh
|
||||
bunx 0x node_modules/wxt/bin/wxt.mjs build
|
||||
```
|
||||
|
||||
- Create a CPU profile:
|
||||
|
||||
```sh
|
||||
bun run --cpu-prof node_modules/wxt/bin/wxt.mjs build
|
||||
```
|
||||
|
||||
- Debug the process:
|
||||
|
||||
```sh
|
||||
bun run --inspect node_modules/wxt/bin/wxt.mjs build
|
||||
```
|
||||
|
||||
## Updating Docs
|
||||
|
||||
Documentation is written with VitePress, and is located in the `docs/` directory.
|
||||
@@ -96,7 +138,7 @@ WXT has unit and E2E tests. When making a change or adding a feature, make sure
|
||||
To run tests for a specific file, add the filename at the end of the test command:
|
||||
|
||||
```sh
|
||||
pnpm test manifest-contents
|
||||
bun run --filter wxt test manifest-contents
|
||||
```
|
||||
|
||||
All test (unit and E2E) for all packages are ran together via [Vitest workspaces](https://vitest.dev/guide/#workspaces-support).
|
||||
@@ -105,7 +147,7 @@ If you want to manually test a change, you can modify the demo project for your
|
||||
|
||||
## Templates
|
||||
|
||||
Each directory inside `templates/` is it's own standalone project. Simply `cd` into the directory you're updating, install dependencies with `npm` (NOT `pnpm`), and run the relevant commands
|
||||
Each directory inside `templates/` is it's own standalone project. Simply `cd` into the directory you're updating, install dependencies with `npm` (NOT `bun`), and run the relevant commands
|
||||
|
||||
```sh
|
||||
cd templates/vue
|
||||
@@ -121,7 +163,7 @@ Note that templates are hardcoded to a specific version of `wxt` from NPM, they
|
||||
"typescript": "^5.3.2",
|
||||
"vite-plugin-solid": "^2.7.0",
|
||||
- "wxt": "^0.8.0"
|
||||
+ "wxt": "../.."
|
||||
+ "wxt": "../../packages/wxt"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -146,17 +188,21 @@ Releases are done with GitHub actions:
|
||||
|
||||
## Upgrading Dependencies
|
||||
|
||||
Use [`taze`](https://www.npmjs.com/package/taze) to upgrade dependencies throughout the entire monorepo.
|
||||
WXT has custom rules around what dependencies can be upgraded. Use the `scripts/upgrade-deps.ts` script to upgrade dependencies and follow these rules.
|
||||
|
||||
```sh
|
||||
pnpm dlx taze -r
|
||||
bun run scripts/upgrade-deps.ts
|
||||
```
|
||||
|
||||
Configuration is in [`taze.config.ts`](./taze.config.ts).
|
||||
To see all the options, run:
|
||||
|
||||
```sh
|
||||
bun run scripts/upgrade-deps.ts --help
|
||||
```
|
||||
|
||||
## Install Unreleased Versions
|
||||
|
||||
This repo uses https://pkg.pr.new to publish versions of all it's packages for almost every commit. You can install them via:
|
||||
This repo uses <https://pkg.pr.new> to publish versions of all it's packages for almost every commit. You can install them via:
|
||||
|
||||
```sh
|
||||
npm i https://pkg.pr.new/[package-name]@[ref]
|
||||
@@ -177,10 +223,16 @@ npm i https://pkg.pr.new/@wxt-dev/module-react@426f907
|
||||
|
||||
## Blog Posts
|
||||
|
||||
Anyone is welcome to submit a blog post on https://wxt.dev/blog!
|
||||
Anyone is welcome to submit a blog post on <https://wxt.dev/blog>!
|
||||
|
||||
> [!NOTE]
|
||||
> Before starting on a blog post, please message Aaron on Discord or start a discussion on GitHub to get permission to write about a topic, but most topics are welcome: Major version updates, tutorials, etc.
|
||||
|
||||
- **English only**: Blog posts should be written in English. Unfortunately, our maintainers doesn't have the bandwidth right now to translate our docs, let alone blog posts. Sorry 😓
|
||||
- **English only**: Blog posts should be written in English. Unfortunately, our maintainers don't have the bandwidth right now to translate our docs, let alone blog posts. Sorry 😓
|
||||
- **AI**: Please only use AI to translate or proof-read your blog post. Don't generate the whole thing... We don't want to publish that.
|
||||
|
||||
## Become a Maintainer
|
||||
|
||||
If you're interested in becoming a maintainer, send an email to Aaron at <aaronklinker1@gmail.com> with your github username saying you're interested. The process is very informal, I will add you quickly if you've contributed code or answered questions and helped out the community!
|
||||
|
||||
Maintainers don't have to just write code - they can manage issues, answer questions, review PRs, organize and prioritize work - there's lots of ways for you to help out.
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Maintainers
|
||||
|
||||
A couple of things for you to consider before merging a PR or giving the go-ahead on a feature proposal.
|
||||
|
||||
## Be picky about new features and packages
|
||||
|
||||
We are responsible for maintaining them and fixing related bugs after the PR is merged. The community can always release their own WXT modules or packages, not everything needs to be built into WXT.
|
||||
|
||||
## Prefer standards over customization
|
||||
|
||||
Don't merge PRs that just add another way to do something, like [this one](https://github.com/wxt-dev/wxt/pull/2053#issuecomment-3857010196).
|
||||
|
||||
WXT is opinionated, if you have questions about what is WXT's opinion or we need to create a new one, create an issue and @ me to discuss.
|
||||
|
||||
## PRs should be small and targeted
|
||||
|
||||
A PR should make one change. They should not make any unrelated changes outside of accomplishing the one thing. This makes PRs easier to review and they get merged more quickly - a win-win for everyone.
|
||||
|
||||
## `@wxt-dev/*` packages are separate
|
||||
|
||||
We can't make changes to these packages assuming people are using them only with WXT. For example, I almost missed [this PR](https://github.com/wxt-dev/wxt/pull/2049#issuecomment-3861251599).
|
||||
|
||||
I want these packages to be usable on their own so if people don't like WXT's build tool, they can still use our other awesome packages.
|
||||
|
||||
## Minimize dependencies
|
||||
|
||||
I don't like how heavy lots of frameworks are. It's unavoidable to a certain extent, but if you can do something without another dependency, try to.
|
||||
|
||||
If you need a dependency, look for one with zero dependencies and that's respectable.
|
||||
|
||||
## Look at milestones
|
||||
|
||||
I've organized WXT's long term plans into [milestones](https://github.com/wxt-dev/wxt/milestones), check those out to get an idea of my priorities for some of the larger features.
|
||||
|
||||
## Require tests
|
||||
|
||||
If someone opens a PR to fix a bug but doesn't include tests, don't merge the PR. Tests are required to prevent regressions and maintain a codebase long term.
|
||||
|
||||
## Ask for reproductions for bugs
|
||||
|
||||
You don't need to triage bugs if someone doesn't give you enough information. You can always ask for a repo with a reproduction or wait for more details.
|
||||
|
||||
If there's not an easy way to reproduce a bug, you're wasting your time triaging it. Just be mindful of your own time!
|
||||
|
||||
Here's an example of how to ask for a reproduction: <https://github.com/wxt-dev/wxt/issues/2064#issuecomment-3862579110>. You could be more blunt than this.
|
||||
|
||||
## Add yourself as a code owner
|
||||
|
||||
If you want to be responsible for a specific package or directory, add yourself to the [`.github/CODEOWNERS`](https://github.com/wxt-dev/wxt/blob/main/.github/CODEOWNERS) file to get added as a reviewer to PRs automatically. You can also add yourself to the default list to be added as a reviewer on all PRs.
|
||||
|
||||
## Releasing Package Updates
|
||||
|
||||
Releases are done with GitHub actions:
|
||||
|
||||
- Use the [Release workflow](https://github.com/wxt-dev/wxt/actions/workflows/release.yml) to release a single package in the monorepo. This automatically detects the version change with conventional commits, builds and uploads the package to NPM, and creates a GitHub release.
|
||||
- Use the [Sync Releases workflow](https://github.com/wxt-dev/wxt/actions/workflows/sync-releases.yml) to sync the GitHub releases with changes to the changelog. To change a release, update the `CHANGELOG.md` file and run the workflow. It will sync the releases of a single package in the monorepo.
|
||||
|
||||
## Creating New Packages
|
||||
|
||||
Example PR: <https://github.com/wxt-dev/wxt/pull/2152>
|
||||
|
||||
1. Create the package.
|
||||
|
||||
2. Update CI workflow inputs.
|
||||
|
||||
3. Add docs page and version for "Other Packages" dropdown.
|
||||
|
||||
4. Merge the PR.
|
||||
|
||||
5. Tag the commit (look at other tags for pattern):
|
||||
|
||||
```sh
|
||||
git tag <dir-name>-v<version>
|
||||
git push --tags
|
||||
```
|
||||
|
||||
6. Publish the package to NPM:
|
||||
|
||||
```sh
|
||||
cd packages/<dir-name>
|
||||
bun publish --access public
|
||||
```
|
||||
|
||||
7. Create a basic release on GitHub mentioning the new package is available.
|
||||
|
||||
A couple of things to note:
|
||||
|
||||
- pkg.pr.new will fail on the original PR. It's fine to ignore and merge your PR as long as it fails due to your new package not being published to NPM yet.
|
||||
- The regular release workflow DOES NOT WORK for new packages. You have to have at least one `<dir-name>-v<version>` tag created before you can run that workflow for your new package.
|
||||
- You don't need to create a CHANGELOG.md file for the package, it will be created automatically after future changes are released via the normal release workflow.
|
||||
@@ -1,50 +1,32 @@
|
||||
<h1 align="center">
|
||||
<img align="top" width="44" src="https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<span>WXT</span>
|
||||
</h1>
|
||||
<div align="center">
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm version" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048"></a>
|
||||
<span> </span>
|
||||
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="downloads" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048"></a>
|
||||
<span> </span>
|
||||
<a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE" target="_blank"><img alt="license | MIT" 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="coverage" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048"></a>
|
||||
</p>
|
||||
# <img align="top" width="44" src="https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg" alt="WXT Logo"> WXT
|
||||
|
||||
<p align="center">
|
||||
<span>Next-gen framework for developing web extensions.</span>
|
||||
<br/>
|
||||
<span>⚡</span>
|
||||
<br/>
|
||||
<q><i>It's like Nuxt, but for Web Extensions</i></q>
|
||||
</p>
|
||||
[](https://www.npmjs.com/package/wxt)
|
||||
[](https://www.npmjs.com/package/wxt)
|
||||
[](https://github.com/wxt-dev/wxt/blob/main/LICENSE)
|
||||
[](https://codecov.io/github/wxt-dev/wxt)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
•
|
||||
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
|
||||
•
|
||||
<a href="https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md" target="_blank">Changelog</a>
|
||||
•
|
||||
<a href="https://discord.gg/ZFsZqGery9" target="_blank">Discord</a>
|
||||
</p>
|
||||
Next-gen framework for developing web extensions.<br/>⚡<br/><q><i>It's like Nuxt, but for Web Extensions</i></q>
|
||||
|
||||
[Get Started](https://wxt.dev/guide/installation.html) •
|
||||
[Configuration](https://wxt.dev/api/config.html) •
|
||||
[Examples](https://wxt.dev/examples.html) •
|
||||
[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md) •
|
||||
[Discord](https://discord.gg/ZFsZqGery9)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
## Demo
|
||||
|
||||
https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94
|
||||
<https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94>
|
||||
|
||||
## Quick Start
|
||||
|
||||
Bootstrap a new project:
|
||||
|
||||
<!-- automd:pm-x version="latest" name="wxt" args="init" -->
|
||||
|
||||
```sh
|
||||
# npm
|
||||
npx wxt@latest init
|
||||
@@ -56,8 +38,6 @@ pnpm dlx wxt@latest init
|
||||
bunx wxt@latest init
|
||||
```
|
||||
|
||||
<!-- /automd -->
|
||||
|
||||
Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
|
||||
|
||||
## Features
|
||||
@@ -79,17 +59,11 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
|
||||
|
||||
WXT is a [MIT-licensed](https://github.com/wxt-dev/wxt/blob/main/LICENSE) open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider [sponsoring WXT's development](https://github.com/sponsors/wxt-dev).
|
||||
|
||||
<a href="https://github.com/sponsors/wxt-dev"><img alt="WXT Sponsors" src="https://raw.githubusercontent.com/wxt-dev/static/refs/heads/main/sponsorkit/sponsors.svg"></a>
|
||||
[](https://github.com/sponsors/wxt-dev)
|
||||
|
||||
## Contributors
|
||||
|
||||
<!-- automd:contributors author="aklinker1" license="MIT" github="wxt-dev/wxt" -->
|
||||
|
||||
Published under the [MIT](https://github.com/wxt-dev/wxt/blob/main/LICENSE) license.
|
||||
Made by [@aklinker1](https://github.com/aklinker1) and [community](https://github.com/wxt-dev/wxt/graphs/contributors) 💛
|
||||
<br><br>
|
||||
<a href="https://github.com/wxt-dev/wxt/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=wxt-dev/wxt" />
|
||||
</a>
|
||||
|
||||
<!-- /automd -->
|
||||
[](https://github.com/wxt-dev/wxt/graphs/contributors)
|
||||
|
||||
+2
-6
@@ -4,12 +4,8 @@ While WXT is in prerelease, only the latest version will receive security update
|
||||
|
||||
<img alt="npm version" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048">
|
||||
|
||||
<!--
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
If you discover a security vulnerability, please email me at <aaronklinker1@gmail.com>. I will respond within a few days to acknowledge receipt of your report.
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
-->
|
||||
If the vulnerability is accepted, I will open a public issue to track the fix. If the vulnerability is not accepted, no further action will be taken.
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
version: '0.2'
|
||||
ignorePaths:
|
||||
- pnpm-lock.yaml
|
||||
- '**/node_modules/**'
|
||||
- '**/dist/**'
|
||||
- '**/coverage/**'
|
||||
- docs/api/**
|
||||
- '**/CHANGELOG.md'
|
||||
- docs/public/_redirects
|
||||
- packages/browser/src/gen/**
|
||||
- patches/**
|
||||
- '**/*.snap'
|
||||
- docs/assets/extension-showcase.yml
|
||||
- packages/*/stats.html
|
||||
words:
|
||||
- Aabid
|
||||
- aabidk
|
||||
- aklinker
|
||||
- automount
|
||||
- avenir
|
||||
- bidi
|
||||
- buildc
|
||||
- bunx
|
||||
- cachable
|
||||
- cacjs
|
||||
- charmbracelet
|
||||
- chromedriver
|
||||
- Cira
|
||||
- comctx
|
||||
- confbox
|
||||
- configfile
|
||||
- consolas
|
||||
- Cpath
|
||||
- crbug
|
||||
- cros
|
||||
- crunchyroll
|
||||
- cssinjs
|
||||
- CSUI
|
||||
- Csvg
|
||||
- defu
|
||||
- deinit
|
||||
- dequal
|
||||
- dtrace
|
||||
- duckduckgo
|
||||
- esbuild
|
||||
- esque
|
||||
- extglob
|
||||
- favicons
|
||||
- fieldtrials
|
||||
- firefoxdeveloperedition
|
||||
- flamechart
|
||||
- Florian
|
||||
- focusring
|
||||
- frontmatter
|
||||
- giget
|
||||
- grayscale
|
||||
- greyscale
|
||||
- gtag
|
||||
- hckhakegfgenefhikdcfkaaonnclljmf
|
||||
- hookable
|
||||
- iconify
|
||||
- jsons
|
||||
- Kenobi
|
||||
- keystyle
|
||||
- klinker
|
||||
- linkedom
|
||||
- llms
|
||||
- locationchange
|
||||
- magicast
|
||||
- Menlo
|
||||
- menupanel
|
||||
- metas
|
||||
- moderok
|
||||
- mktemp
|
||||
- mycompany
|
||||
- nacl
|
||||
- nanospinner
|
||||
- natsu
|
||||
- newtab
|
||||
- njsproj
|
||||
- nodebuffer
|
||||
- Noto
|
||||
- ntvs
|
||||
- nypm
|
||||
- obug
|
||||
- ohash
|
||||
- oklab
|
||||
- oxlint
|
||||
- pbbf
|
||||
- personaltoolbar
|
||||
- portaling
|
||||
- prebundled
|
||||
- prefs
|
||||
- proxified
|
||||
- pWXLO
|
||||
- repost
|
||||
- rxliuli
|
||||
- sandboxed
|
||||
- scule
|
||||
- Segoe
|
||||
- sidepanel
|
||||
- sidepanels
|
||||
- solidjs
|
||||
- styl
|
||||
- subdependency
|
||||
- tabstrip
|
||||
- teleporting
|
||||
- treemap
|
||||
- triaging
|
||||
- umami
|
||||
- ungh
|
||||
- unimport
|
||||
- unocss
|
||||
- unrun
|
||||
- unstub
|
||||
- unwatch
|
||||
- USSS
|
||||
- vitepress
|
||||
- webext
|
||||
- webextension
|
||||
- webextrc
|
||||
- xcrun
|
||||
- xlink
|
||||
@@ -3,8 +3,9 @@ import { computed } from 'vue';
|
||||
// @ts-expect-error: Vitepress data-loader magic, this import is correct
|
||||
import { data } from '../loaders/blog.data';
|
||||
import BlogPostPreview from './BlogPostPreview.vue';
|
||||
import { Post } from '../utils/types';
|
||||
|
||||
const posts = computed(() =>
|
||||
const posts = computed<Post[]>(() =>
|
||||
data
|
||||
.map((post) => ({
|
||||
...post,
|
||||
@@ -17,10 +18,8 @@ const posts = computed(() =>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div>
|
||||
<div class="vp-doc">
|
||||
<h1>Blog</h1>
|
||||
</div>
|
||||
<div class="inner-container">
|
||||
<h1>Blog</h1>
|
||||
|
||||
<ul>
|
||||
<BlogPostPreview v-for="post of posts" :key="post.url" :post />
|
||||
@@ -34,37 +33,33 @@ const posts = computed(() =>
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.container > div {
|
||||
padding: 32px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.inner-container {
|
||||
padding: 32px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid var(--vp-c-default);
|
||||
}
|
||||
ul li:last-child {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--vp-c-default);
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
li {
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { Ref } from 'vue';
|
||||
import useBlogDate from '../composables/useBlogDate';
|
||||
import { useData } from 'vitepress';
|
||||
import { Content, useData } from 'vitepress';
|
||||
import { PostFrontmatter } from '../utils/types';
|
||||
|
||||
const { frontmatter } = useData() as unknown as {
|
||||
frontmatter: Ref<PostFrontmatter>;
|
||||
};
|
||||
|
||||
const { frontmatter } = useData();
|
||||
const date = useBlogDate(() => frontmatter.value.date);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-doc">
|
||||
<main class="container-content">
|
||||
<h1 v-html="$frontmatter.title" />
|
||||
<main class="container">
|
||||
<h1 v-html="frontmatter.title" />
|
||||
<p class="meta-row">
|
||||
<a
|
||||
class="author"
|
||||
v-for="author of $frontmatter.authors"
|
||||
v-for="author of frontmatter.authors"
|
||||
:key="author.github"
|
||||
:href="`https://github.com/${author.github}`"
|
||||
class="author"
|
||||
>
|
||||
<img :src="`https://github.com/${author.github}.png?size=96`" />
|
||||
<img
|
||||
:src="`https://github.com/${author.github}.png?size=96`"
|
||||
alt="Author's avatar"
|
||||
/>
|
||||
<span>{{ author.name }}</span>
|
||||
</a>
|
||||
<span>•</span>
|
||||
@@ -29,48 +37,43 @@ const date = useBlogDate(() => frontmatter.value.date);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
vp-doc {
|
||||
.vp-doc {
|
||||
display: flex;
|
||||
}
|
||||
main {
|
||||
max-width: 1080px;
|
||||
padding: 32px;
|
||||
margin: auto;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
main {
|
||||
padding: 64px;
|
||||
|
||||
.container {
|
||||
max-width: 1080px;
|
||||
padding: 32px;
|
||||
margin: auto;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
color: var(--vp-c-text-2);
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 32px;
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
|
||||
:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.meta-row {
|
||||
display: flex;
|
||||
color: var(--vp-c-text-2);
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
.meta-row > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.author {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
.author img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
.author span {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.author:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,42 +31,23 @@ const date = useBlogDate(() => props.post.date);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.vp-doc {
|
||||
* {
|
||||
margin-block: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
.title {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
li > a > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -16px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
li > a > div:hover {
|
||||
background: var(--vp-c-default);
|
||||
}
|
||||
li .title {
|
||||
color: var(--vp-c-text);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
li .description {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
li .meta {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
.description {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,40 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
patterns: Array<[intput: string, output: string]>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="no-vertical-dividers">
|
||||
<table class="no-vertical-dividers entrypoint-patterns">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 100%">Filename</th>
|
||||
<th class="col-filename">Filename</th>
|
||||
<th></th>
|
||||
<th>Output Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pattern of patterns">
|
||||
<td style="white-space: nowrap; padding-right: 8px">
|
||||
<tr v-for="pattern of patterns" :key="pattern[0]">
|
||||
<td class="cell-input">
|
||||
<code>entrypoints/{{ pattern[0] }}</code>
|
||||
</td>
|
||||
<td style="padding: 6px; opacity: 50%">
|
||||
<td class="cell-arrow">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4 11v2h12l-5.5 5.5l1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5L16 11H4Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</td>
|
||||
<td style="white-space: nowrap; padding-left: 8px">
|
||||
<td class="cell-output">
|
||||
<code>/{{ pattern[1] }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entrypoint-patterns {
|
||||
width: 100%;
|
||||
|
||||
.col-filename {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cell-input {
|
||||
white-space: nowrap;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.cell-arrow {
|
||||
padding: 6px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cell-output {
|
||||
white-space: nowrap;
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed, toRaw, Ref } from 'vue';
|
||||
import { computed, onMounted, ref, Ref, toRaw } from 'vue';
|
||||
import ExampleSearchFilterByItem from './ExampleSearchFilterByItem.vue';
|
||||
import ExampleSearchResult from './ExampleSearchResult.vue';
|
||||
import { ExamplesMetadata, KeySelectedObject } from '../utils/types';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
const exampleMetadata = ref<ExamplesMetadata>();
|
||||
const exampleMetadata = ref<ExamplesMetadata>({
|
||||
allApis: [],
|
||||
allPackages: [],
|
||||
allPermissions: [],
|
||||
examples: [],
|
||||
});
|
||||
onMounted(async () => {
|
||||
const res = await fetch(
|
||||
'https://raw.githubusercontent.com/wxt-dev/examples/main/metadata.json',
|
||||
@@ -48,7 +53,7 @@ function doesExampleMatchSelected(
|
||||
|
||||
const filteredExamples = computed(() => {
|
||||
const text = searchText.value.toLowerCase();
|
||||
return exampleMetadata.value.examples.filter((example) => {
|
||||
return exampleMetadata.value?.examples.filter((example) => {
|
||||
const matchesText = example.searchText.toLowerCase().includes(text);
|
||||
const matchesApis = doesExampleMatchSelected(example.apis, requiredApis);
|
||||
const matchesPermissions = doesExampleMatchSelected(
|
||||
@@ -72,19 +77,19 @@ const filteredExamples = computed(() => {
|
||||
|
||||
<div class="filters">
|
||||
<ExampleSearchFilterByItem
|
||||
label="APIs"
|
||||
:items="exampleMetadata?.allApis"
|
||||
v-model="selectedApis"
|
||||
:items="exampleMetadata?.allApis"
|
||||
label="APIs"
|
||||
/>
|
||||
<ExampleSearchFilterByItem
|
||||
label="Permissions"
|
||||
:items="exampleMetadata?.allPermissions"
|
||||
v-model="selectedPermissions"
|
||||
:items="exampleMetadata?.allPermissions"
|
||||
label="Permissions"
|
||||
/>
|
||||
<ExampleSearchFilterByItem
|
||||
label="Packages"
|
||||
:items="exampleMetadata?.allPackages"
|
||||
v-model="selectedPackages"
|
||||
:items="exampleMetadata?.allPackages"
|
||||
label="Packages"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -107,120 +112,57 @@ const filteredExamples = computed(() => {
|
||||
<style scoped>
|
||||
.example-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-areas:
|
||||
'search'
|
||||
'results';
|
||||
'filters search'
|
||||
'filters results';
|
||||
gap: 16px;
|
||||
}
|
||||
@media only screen and (min-width: 720px) {
|
||||
.example-layout {
|
||||
|
||||
@media only screen and (min-width: 710px) {
|
||||
grid-template-columns: 256px 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-areas:
|
||||
'filters search'
|
||||
'filters results';
|
||||
}
|
||||
}
|
||||
.search {
|
||||
grid-area: search;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.filters {
|
||||
display: none;
|
||||
grid-area: filters;
|
||||
}
|
||||
@media only screen and (min-width: 720px) {
|
||||
|
||||
.search {
|
||||
grid-area: search;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
border-radius: 16px;
|
||||
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
grid-area: filters;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
align-self: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
@media only screen and (min-width: 710px) {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.results {
|
||||
grid-area: results;
|
||||
}
|
||||
|
||||
.box {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.results {
|
||||
grid-area: results;
|
||||
|
||||
.search input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
}
|
||||
.search-results {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
|
||||
.checkbox-col {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
max-height: 200px;
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
}
|
||||
@media only screen and (min-width: 800px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
@media only screen and (min-width: 1024px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.checkbox-col .header {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.checkbox-col p {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
text-wrap: wrap;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
span {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.checkbox-col input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox-col-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
@media only screen and (min-width: 800px) {
|
||||
.search-results {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
a {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1024px) {
|
||||
.search-results {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, toRaw } from 'vue';
|
||||
import { KeySelectedObject } from '../utils/types';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
label: string;
|
||||
items?: string[];
|
||||
}>();
|
||||
@@ -29,12 +29,12 @@ function toggleItem(pkg: string) {
|
||||
<span>Filter by {{ label }}</span> <span v-if="count">({{ count }})</span>
|
||||
</p>
|
||||
<div class="scroll-container">
|
||||
<ul>
|
||||
<ul class="items">
|
||||
<li v-for="item in items">
|
||||
<label :title="item">
|
||||
<label :title="item" class="item-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedItems[item]"
|
||||
type="checkbox"
|
||||
@input="toggleItem(item)"
|
||||
/>
|
||||
<span>{{ item }}</span>
|
||||
@@ -51,50 +51,45 @@ function toggleItem(pkg: string) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
border-radius: 20px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--vp-c-brand-3) transparent;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.header {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.scroll-container ul {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: small;
|
||||
padding: 8px 16px 16px 16px;
|
||||
}
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.header {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 50%;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
text-wrap: wrap;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 140%;
|
||||
cursor: pointer;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
span {
|
||||
padding-top: 1px;
|
||||
}
|
||||
input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
.items {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: small;
|
||||
padding: 8px 16px 16px;
|
||||
|
||||
.item-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Example } from '../utils/types';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
example: Example;
|
||||
}>();
|
||||
</script>
|
||||
@@ -17,56 +17,45 @@ const props = defineProps<{
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
min-width: 0;
|
||||
}
|
||||
a {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 2px solid var(--vp-c-bg-soft);
|
||||
border-radius: 16px;
|
||||
color: var(--vp-c-text-1) !important;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
outline: 2px solid var(--vp-c-brand-2);
|
||||
}
|
||||
.name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
&:hover {
|
||||
outline: 2px solid var(--vp-c-brand-2);
|
||||
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.description {
|
||||
height: 53px;
|
||||
opacity: 70%;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 120%;
|
||||
min-height: 0;
|
||||
.link {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.name {
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.link {
|
||||
opacity: 0;
|
||||
transition: 250ms;
|
||||
color: var(--vp-c-brand-2);
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
a:hover .link {
|
||||
opacity: 100%;
|
||||
.description {
|
||||
opacity: 70%;
|
||||
font-size: 14px;
|
||||
line-height: 130%;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link {
|
||||
opacity: 0;
|
||||
transition: 250ms;
|
||||
color: var(--vp-c-brand-2);
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,121 +1,138 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import useListExtensionDetails, {
|
||||
ChromeExtension,
|
||||
} from '../composables/useListExtensionDetails';
|
||||
import useListExtensionDetails from '../composables/useListExtensionDetails';
|
||||
import _extensionEntries from '../../assets/extension-showcase.yml';
|
||||
|
||||
// Add extension IDs here. Order doesn't matter, will be sorted by a combination of weekly active users and rating.
|
||||
// Change the commit message or PR title to: "docs: Added "[extension name]" to the homepage"
|
||||
const chromeExtensionIds = [
|
||||
'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts
|
||||
'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player
|
||||
'bfbnagnphiehemkdgmmficmjfddgfhpl', // UltraWideo
|
||||
'elfaihghhjjoknimpccccmkioofjjfkf', // StayFree - Website Blocker & Web Analytics
|
||||
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
|
||||
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
'oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
|
||||
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
|
||||
'aiakblgmlabokilgljkglggnpflljdgp', // Markdown Sticky Notes
|
||||
'nomnkbngkijpffepcgbbofhcnafpkiep', // DocVersionRedirector
|
||||
'ceicccfeikoipigeghddpocceifjelph', // Plex Skipper
|
||||
'aelkipgppclpfimeamgmlonimflbhlgf', // GitHub Custom Notifier
|
||||
'djnlaiohfaaifbibleebjggkghlmcpcj', // Fluent Read
|
||||
'nhclljcpfmmaiojbhhnkpjcfmacfcian', // Facebook Video Controls
|
||||
'mblkhbaakhbhiimkbcnmeciblfhmafna', // ElemSnap - Quick capture of webpage elements and conversion to images,
|
||||
'oajalfneblkfiejoadecnmodfpnaeblh', // MS Edge TTS (Text to Speech)
|
||||
'nedcanggplmbbgmlpcjiafgjcpdimpea', // YTBlock - Block any content from YouTube™
|
||||
'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert
|
||||
'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
|
||||
'khjdmjcmpolknpccmaaipmidphjokhdf', // WorkFlowy MultiFlow
|
||||
'fencadnndhdeggodopebjgdfdlhcimfk', // 香草布丁🌿🍮- https://github.com/Xdy1579883916/vanilla-pudding
|
||||
'bnacincmbaknlbegecpioobkfgejlojp', // MaxFocus: Link Preview
|
||||
'bcpgdpedphodjcjlminjbdeejccjbimp', // 汇率转换-中文版本
|
||||
'loeilaonggnalkaiiaepbegccilkmjjp', // Currency Converter Plus
|
||||
'npcnninnjghigjfiecefheeibomjpkak', // Respond Easy
|
||||
'cfkdcideecefncbglkhneoflfnmhoicc', // mindful - stay focused on your goals
|
||||
'lnhejcpclabmbgpiiomjbhalblnnbffg', // 1Proompt
|
||||
'fonflmjnjbkigocpoommgmhljdpljain', // NiceTab - https://github.com/web-dahuyou/NiceTab
|
||||
'fcffekbnfcfdemeekijbbmgmkognnmkd', // Draftly for LinkedIn
|
||||
'nkndldfehcidpejfkokbeghpnlbppdmo', // YouTube Summarized - Summarize any YouTube video
|
||||
'dbichmdlbjdeplpkhcejgkakobjbjalc', // 社媒助手 - https://github.com/iszhouhua/social-media-copilot
|
||||
'opepfpjeogkbgeigkbepobceinnfmjdd', // Dofollow Links for SEO
|
||||
'pdnenlnelpdomajfejgapbdpmjkfpjkp', // ChatGPT Writer: Use AI on Any Site (GPT-4o, Claude, Gemini, and More)
|
||||
'jobnhifpphkgoelnhnopgkdhbdkiadmj', // discord message translator
|
||||
'ncokhechhpjgjonhjnlaneglmdkfkcbj', // Habit Tracker app widget for daily habit tracking
|
||||
'lnjaiaapbakfhlbjenjkhffcdpoompki', // Catppuccin for GitHub File Explorer Icons
|
||||
'cpaedhbidlpnbdfegakhiamfpndhjpgf', // WebChat: Chat with anyone on any website
|
||||
'fcphghnknhkimeagdglkljinmpbagone', // YouTube Auto HD + FPS
|
||||
'lpomjgbicdemjkgmbnkjncgdebogkhlb', // MultiViewer Companion
|
||||
'ggiafipgeeaaahnjamgpjcgkdpanhddg', // Sync Watch - Watch videos together on any site
|
||||
'nmldnjcblcihmegipecakhmnieiofmgl', // Keyword Rank Checker
|
||||
'gppllamhaciichleihemgilcpledblpn', // YouTube Simple View - Hide distractions & more
|
||||
'pccbghdfdnnkkbcdcibchpbffdgednkf', // Propbar - Property Data Enhancer
|
||||
'lfknakglefggmdkjdfhhofkjnnolffkh', // Text Search Pro - Search by case and whole-word match!
|
||||
'mbenhbocjckkbaojacmaepiameldglij', // Invoice Generator
|
||||
'phlfhkmdofajnbhgmbmjkbkdgppgoppb', // Monthly Bill Tracker
|
||||
'macmkmchfoclhpbncclinhjflmdkaoom', // Wandpen - Instantly improve your writing with AI
|
||||
'lhmgechokhmdekdpgkkemoeecelcaonm', // YouTube Hider - Remove Comments By Keywords, Usernames & Tools
|
||||
'imgheieooppmahcgniieddodaliodeeg', // QA Compass - Record standardized bug reports easily
|
||||
'npgghjedpchajflknnbngajkjkdhncdo', // aesthetic Notion, styled
|
||||
'hmdcmlfkchdmnmnmheododdhjedfccka', // Eye Dropper
|
||||
'eihpmapodnppeemkhkbhikmggfojdkjd', // Cursorful - Screen Recorder with Auto Zoom
|
||||
'hjjkgbibknbahijglkffklflidncplkn', // Show IP – Live View of Website IPs for Developers
|
||||
'ilbikcehnpkmldojkcmlldkoelofnbde', // Strong Password Generator
|
||||
'ocllfkhcdopiafndigclebelbecaiocp', // ZenGram: Mindful Instagram, Your Way
|
||||
'odffpjnpocjfcaclnenaaaddghkgijdb', // Blync: Preview Links, Selection Search, AI Assistant
|
||||
'kofbbilhmnkcmibjbioafflgmpkbnmme', // HTML to Markdown - Convert webpages to markdown
|
||||
'boecmgggeigllcdocgioijmleimjbfkg', // Walmart WFS Profit Calculator
|
||||
'dlnjcbkmomenmieechnmgglgcljhoepd', // Youtube Live Chat Fullscreen
|
||||
'keiealdacakpnbbljlmhfgcebmaadieg', // Python Code Runner
|
||||
'hafcajcllbjnoolpfngclfmmgpikdhlm', // Monochromate
|
||||
'bmoggiinmnodjphdjnmpcnlleamkfedj', // AliasVault - Open-Source Password & (Email) Alias Manager
|
||||
'hlnhhamckimoaiekbglafiebkfimhapb', // SnapThePrice: AI-Powered Real-time Lowest Price Finder
|
||||
interface StoreLink {
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ListedExtension {
|
||||
id: string;
|
||||
name: string;
|
||||
iconUrl: string;
|
||||
shortDescription: string;
|
||||
users: number;
|
||||
rating: number | undefined;
|
||||
stores: StoreLink[];
|
||||
}
|
||||
|
||||
const extensionEntries = _extensionEntries as Array<{
|
||||
chromeId?: string;
|
||||
firefoxSlug?: string;
|
||||
edgeId?: string;
|
||||
}>;
|
||||
|
||||
const chromeIds = extensionEntries.flatMap((e) =>
|
||||
e.chromeId ? [e.chromeId] : [],
|
||||
);
|
||||
const firefoxSlugs = [
|
||||
...new Set(
|
||||
extensionEntries.flatMap((e) => (e.firefoxSlug ? [e.firefoxSlug] : [])),
|
||||
),
|
||||
];
|
||||
const edgeIds = extensionEntries.flatMap((e) => (e.edgeId ? [e.edgeId] : []));
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
const sortedExtensions = computed(() => {
|
||||
if (!data.value?.length) return [];
|
||||
const { data, isLoading } = useListExtensionDetails(
|
||||
chromeIds,
|
||||
firefoxSlugs,
|
||||
edgeIds,
|
||||
);
|
||||
|
||||
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);
|
||||
function addUtmSource(storeUrl: string): string {
|
||||
const url = new URL(storeUrl);
|
||||
url.searchParams.set('utm_source', 'wxt.dev');
|
||||
return url.href;
|
||||
}
|
||||
|
||||
const sortedExtensions = computed((): ListedExtension[] => {
|
||||
if (!data.value) return [];
|
||||
|
||||
const chromeById = new Map(
|
||||
(data.value.chrome ?? []).filter(Boolean).map((e) => [e.id, e]),
|
||||
);
|
||||
const firefoxBySlug = new Map(
|
||||
(data.value.firefox ?? []).filter(Boolean).map((e) => [e.id, e]),
|
||||
);
|
||||
const edgeById = new Map(
|
||||
(data.value.edge ?? []).filter(Boolean).map((e) => [e.id, e]),
|
||||
);
|
||||
|
||||
const results: ListedExtension[] = [];
|
||||
|
||||
for (const entry of extensionEntries) {
|
||||
const chrome = entry.chromeId ? chromeById.get(entry.chromeId) : undefined;
|
||||
const firefox = entry.firefoxSlug
|
||||
? firefoxBySlug.get(entry.firefoxSlug)
|
||||
: undefined;
|
||||
const edge = entry.edgeId ? edgeById.get(entry.edgeId) : undefined;
|
||||
|
||||
if (!chrome && !firefox && !edge) continue;
|
||||
|
||||
const primary = chrome ?? firefox ?? edge!;
|
||||
const users =
|
||||
(chrome?.users ?? 0) + (firefox?.users ?? 0) + (edge?.users ?? 0);
|
||||
const rating = chrome?.rating ?? firefox?.rating ?? edge?.rating;
|
||||
|
||||
const stores: StoreLink[] = [];
|
||||
if (chrome) {
|
||||
stores.push({ label: 'Chrome', url: addUtmSource(chrome.storeUrl) });
|
||||
}
|
||||
if (firefox) {
|
||||
stores.push({ label: 'Firefox', url: addUtmSource(firefox.storeUrl) });
|
||||
}
|
||||
if (edge) {
|
||||
stores.push({ label: 'Edge', url: addUtmSource(edge.storeUrl) });
|
||||
}
|
||||
|
||||
results.push({
|
||||
id:
|
||||
entry.chromeId ??
|
||||
(entry.firefoxSlug
|
||||
? `firefox:${entry.firefoxSlug}`
|
||||
: `edge:${entry.edgeId}`),
|
||||
name: primary.name,
|
||||
iconUrl: primary.iconUrl,
|
||||
shortDescription: primary.shortDescription,
|
||||
users,
|
||||
rating,
|
||||
stores,
|
||||
});
|
||||
}
|
||||
|
||||
const score = (e: ListedExtension) => ((e.rating ?? 5) / 5) * e.users;
|
||||
return results.sort((a, b) => score(b) - score(a));
|
||||
});
|
||||
|
||||
function formatUsers(n: number): string {
|
||||
return `${n.toLocaleString()} users`;
|
||||
}
|
||||
|
||||
function formatStars(r: number): string {
|
||||
const rounded = Math.round(r * 10) / 10;
|
||||
return `${rounded % 1 === 0 ? rounded.toFixed(0) : rounded.toFixed(1)} stars`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p v-if="isLoading" style="text-align: center; opacity: 50%">Loading...</p>
|
||||
<p
|
||||
v-else-if="err || sortedExtensions.length === 0"
|
||||
v-else-if="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"
|
||||
>
|
||||
<ul v-else class="extension-grid">
|
||||
<li v-for="extension of sortedExtensions" :key="extension.id">
|
||||
<img
|
||||
:src="extension.iconUrl"
|
||||
:alt="`${extension.name} icon`"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<div class="relative">
|
||||
<div class="card-content">
|
||||
<a
|
||||
:href="getStoreUrl(extension)"
|
||||
:href="extension.stores[0]?.url ?? '#'"
|
||||
target="_blank"
|
||||
:title="extension.name"
|
||||
class="extension-name"
|
||||
@@ -124,19 +141,39 @@ function getStoreUrl(extension: ChromeExtension) {
|
||||
<p class="description" :title="extension.shortDescription">
|
||||
{{ extension.shortDescription }}
|
||||
</p>
|
||||
<div v-if="extension.stores.length > 0" class="store-stats">
|
||||
<p class="store-stats-info">
|
||||
<span>{{ formatUsers(extension.users) }}</span>
|
||||
<template v-if="extension.rating">
|
||||
<span class="store-stats-sep" aria-hidden="true">,</span>
|
||||
<span>{{ formatStars(extension.rating) }}</span>
|
||||
</template>
|
||||
</p>
|
||||
<p class="store-stats-sep" aria-hidden="true">·</p>
|
||||
<p class="store-links">
|
||||
<template v-for="(store, i) of extension.stores" :key="store.label">
|
||||
<a
|
||||
:href="store.url"
|
||||
target="_blank"
|
||||
class="store-link"
|
||||
:title="store.label"
|
||||
>{{ store.label }}</a
|
||||
>
|
||||
<span
|
||||
v-if="i < extension.stores.length - 1"
|
||||
class="store-stats-sep"
|
||||
aria-hidden="true"
|
||||
>, </span
|
||||
>
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="user-count">
|
||||
<span>{{ extension.weeklyActiveUsers.toLocaleString() }} users</span
|
||||
><template v-if="extension.rating != null"
|
||||
>,
|
||||
<span>{{ extension.rating }} stars</span>
|
||||
</template>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="centered pr">
|
||||
<a
|
||||
href="https://github.com/wxt-dev/wxt/edit/main/docs/.vitepress/components/UsingWxtSection.vue"
|
||||
href="https://github.com/wxt-dev/wxt/edit/main/docs/assets/extension-showcase.yml"
|
||||
target="_blank"
|
||||
>Open a PR</a
|
||||
>
|
||||
@@ -145,7 +182,7 @@ function getStoreUrl(extension: ChromeExtension) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
li img {
|
||||
.extension-grid > li > img {
|
||||
width: 116px;
|
||||
height: 116px;
|
||||
padding: 16px;
|
||||
@@ -153,7 +190,7 @@ li img {
|
||||
background-color: var(--vp-c-default-soft);
|
||||
}
|
||||
|
||||
ul {
|
||||
.extension-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
align-items: stretch;
|
||||
@@ -163,12 +200,12 @@ ul {
|
||||
padding: 0;
|
||||
}
|
||||
@media (min-width: 960px) {
|
||||
ul {
|
||||
.extension-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
.extension-grid > li {
|
||||
margin: 0 !important;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
@@ -176,28 +213,68 @@ li {
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
li a,
|
||||
li .user-count,
|
||||
li .description {
|
||||
.extension-grid > li .description {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
li .user-count {
|
||||
opacity: 70%;
|
||||
font-size: small;
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 16px;
|
||||
|
||||
.extension-grid > li .card-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
li a {
|
||||
.store-stats {
|
||||
margin-top: auto;
|
||||
padding-top: 6px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: baseline;
|
||||
column-gap: 6px;
|
||||
row-gap: 4px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
opacity: 0.72;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.store-stats-info {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.store-stats-sep {
|
||||
opacity: 0.45;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.store-links {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.store-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.store-link:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.extension-grid > li .extension-name {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
@@ -206,33 +283,23 @@ li a {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
font-size: large;
|
||||
}
|
||||
li a:hover {
|
||||
|
||||
.extension-grid > li .extension-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
li div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
li .description {
|
||||
.extension-grid > li .description {
|
||||
opacity: 90%;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
li .extension-name {
|
||||
font-size: large;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.pr {
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,47 +1,84 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export interface ChromeExtension {
|
||||
export interface Extension {
|
||||
id: string;
|
||||
name: string;
|
||||
iconUrl: string;
|
||||
weeklyActiveUsers: number;
|
||||
shortDescription: string;
|
||||
storeUrl: string;
|
||||
rating: number | undefined;
|
||||
users: number;
|
||||
}
|
||||
|
||||
export interface ExtensionResults {
|
||||
chrome: Extension[];
|
||||
firefox: Extension[];
|
||||
edge: Extension[];
|
||||
}
|
||||
|
||||
const operationName = 'WxtDocsUsedBy';
|
||||
const query = `query ${operationName}($ids:[String!]!) {
|
||||
chromeExtensions(ids: $ids) {
|
||||
const query = `query ${operationName}($chromeIds: [String!]!, $firefoxIds: [String!]!, $edgeIds: [String!]!) {
|
||||
chromeExtensions(ids: $chromeIds) {
|
||||
id
|
||||
name
|
||||
iconUrl
|
||||
weeklyActiveUsers
|
||||
shortDescription
|
||||
storeUrl
|
||||
rating
|
||||
...ExtensionData
|
||||
}
|
||||
firefoxAddons(ids: $firefoxIds) {
|
||||
id: slug
|
||||
...ExtensionData
|
||||
}
|
||||
edgeAddons(ids: $edgeIds) {
|
||||
id
|
||||
...ExtensionData
|
||||
}
|
||||
}
|
||||
|
||||
fragment ExtensionData on Extension {
|
||||
name
|
||||
iconUrl
|
||||
shortDescription
|
||||
storeUrl
|
||||
rating
|
||||
users
|
||||
}`;
|
||||
|
||||
export default function (ids: string[]) {
|
||||
const data = ref<ChromeExtension[]>();
|
||||
export default function (
|
||||
chromeIds: string[],
|
||||
firefoxSlugs: string[],
|
||||
edgeIds: string[],
|
||||
) {
|
||||
const data = ref<ExtensionResults>();
|
||||
const err = ref<unknown>();
|
||||
const isLoading = ref(true);
|
||||
|
||||
if (
|
||||
chromeIds.length === 0 &&
|
||||
firefoxSlugs.length === 0 &&
|
||||
edgeIds.length === 0
|
||||
) {
|
||||
data.value = { chrome: [], firefox: [], edge: [] };
|
||||
isLoading.value = false;
|
||||
return { data, err, isLoading };
|
||||
}
|
||||
|
||||
fetch('https://queue.wxt.dev/api', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
operationName,
|
||||
query,
|
||||
variables: { ids },
|
||||
variables: { chromeIds, firefoxIds: firefoxSlugs, edgeIds },
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(async (res) => {
|
||||
isLoading.value = false;
|
||||
const {
|
||||
data: { chromeExtensions },
|
||||
} = await res.json();
|
||||
data.value = chromeExtensions;
|
||||
const { data: responseData } = await res.json();
|
||||
data.value = {
|
||||
chrome: responseData.chromeExtensions ?? [],
|
||||
firefox: responseData.firefoxAddons ?? [],
|
||||
edge: responseData.edgeAddons ?? [],
|
||||
};
|
||||
err.value = undefined;
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -51,9 +88,5 @@ export default function (ids: string[]) {
|
||||
err.value = error;
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
err,
|
||||
isLoading,
|
||||
};
|
||||
return { data, err, isLoading };
|
||||
}
|
||||
|
||||
+67
-33
@@ -1,5 +1,26 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { Feed } from 'feed';
|
||||
// @ts-expect-error; It isn't TypeScript lib
|
||||
import footnote from 'markdown-it-footnote';
|
||||
import fs, { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { DefaultTheme, defineConfig } from 'vitepress';
|
||||
import addKnowledge from 'vitepress-knowledge';
|
||||
import {
|
||||
groupIconMdPlugin,
|
||||
groupIconVitePlugin,
|
||||
localIconLoader,
|
||||
} from 'vitepress-plugin-group-icons';
|
||||
import llmstxt from 'vitepress-plugin-llms';
|
||||
import { version as analyticsVersion } from '../../packages/analytics/package.json';
|
||||
import { version as autoIconsVersion } from '../../packages/auto-icons/package.json';
|
||||
import { version as i18nVersion } from '../../packages/i18n/package.json';
|
||||
import { version as runnerVersion } from '../../packages/runner/package.json';
|
||||
import { version as storageVersion } from '../../packages/storage/package.json';
|
||||
import { version as unocssVersion } from '../../packages/unocss/package.json';
|
||||
import { version as wxtVersion } from '../../packages/wxt/package.json';
|
||||
import { version as isBackgroundVersion } from '../../packages/is-background/package.json';
|
||||
import typedocSidebar from '../api/reference/typedoc-sidebar.json';
|
||||
import { meta, script } from './utils/head';
|
||||
import {
|
||||
menuGroup,
|
||||
menuItem,
|
||||
@@ -7,23 +28,6 @@ import {
|
||||
navItem,
|
||||
prepareTypedocSidebar,
|
||||
} from './utils/menus';
|
||||
import { meta, script } from './utils/head';
|
||||
import footnote from 'markdown-it-footnote';
|
||||
import { version as wxtVersion } from '../../packages/wxt/package.json';
|
||||
import { version as i18nVersion } from '../../packages/i18n/package.json';
|
||||
import { version as autoIconsVersion } from '../../packages/auto-icons/package.json';
|
||||
import { version as unocssVersion } from '../../packages/unocss/package.json';
|
||||
import { version as storageVersion } from '../../packages/storage/package.json';
|
||||
import { version as analyticsVersion } from '../../packages/analytics/package.json';
|
||||
import knowledge from 'vitepress-knowledge';
|
||||
import {
|
||||
groupIconMdPlugin,
|
||||
groupIconVitePlugin,
|
||||
localIconLoader,
|
||||
} from 'vitepress-plugin-group-icons';
|
||||
import { Feed } from 'feed';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const origin = 'https://wxt.dev';
|
||||
|
||||
@@ -41,27 +45,40 @@ const otherPackages = {
|
||||
i18n: i18nVersion,
|
||||
storage: storageVersion,
|
||||
unocss: unocssVersion,
|
||||
runner: runnerVersion,
|
||||
'is-background': isBackgroundVersion,
|
||||
};
|
||||
|
||||
const knowledge = addKnowledge<DefaultTheme.Config>({
|
||||
serverUrl: 'https://knowledge.wxt.dev',
|
||||
paths: {
|
||||
'/': 'docs',
|
||||
'/api/': 'api-reference',
|
||||
'/blog/': 'blog',
|
||||
},
|
||||
layoutSelectors: {
|
||||
blog: '.container-content',
|
||||
},
|
||||
pageSelectors: {
|
||||
'examples.md': '#VPContent > .VPPage',
|
||||
'blog.md': '#VPContent > .VPPage',
|
||||
},
|
||||
});
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
extends: knowledge({
|
||||
serverUrl: 'https://knowledge.wxt.dev',
|
||||
paths: {
|
||||
'/': 'docs',
|
||||
'/api/': 'api-reference',
|
||||
},
|
||||
pageSelectors: {
|
||||
'examples.md': '#VPContent > .VPPage',
|
||||
},
|
||||
}),
|
||||
extends: knowledge,
|
||||
|
||||
titleTemplate: `:title${titleSuffix}`,
|
||||
title: 'WXT',
|
||||
description,
|
||||
vite: {
|
||||
clearScreen: false,
|
||||
//TODO: REMOVE THIS @TS-EXPECT-ERROR AFTER BUMP VITEPRESS TO V2.0
|
||||
plugins: [
|
||||
// @ts-expect-error: Vite version mismatch between this project and the plugin
|
||||
llmstxt(),
|
||||
// @ts-expect-error: Vite version mismatch between this project and the plugin
|
||||
groupIconVitePlugin({
|
||||
customIcon: {
|
||||
'wxt.config.ts': localIconLoader(
|
||||
@@ -70,6 +87,17 @@ export default defineConfig({
|
||||
),
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'yaml-loader',
|
||||
load: {
|
||||
handler: async (id) => {
|
||||
if (id.endsWith('.yml') || id.endsWith('.yaml')) {
|
||||
const obj = Bun.YAML.parse(await readFile(id, 'utf8'));
|
||||
return `export default ${JSON.stringify(obj)}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
lastUpdated: true,
|
||||
@@ -78,6 +106,9 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
async buildEnd(site) {
|
||||
// @ts-expect-error: knowledge.buildEnd is not typed, but it exists.
|
||||
await knowledge.buildEnd(site);
|
||||
|
||||
// Only construct the RSS document for production builds
|
||||
const { default: blogDataLoader } = await import('./loaders/blog.data');
|
||||
const posts = await blogDataLoader.load();
|
||||
@@ -95,9 +126,9 @@ export default defineConfig({
|
||||
description: post.frontmatter.description,
|
||||
});
|
||||
});
|
||||
console.log('rss.xml:');
|
||||
console.log(feed.rss2());
|
||||
await writeFile(join(site.outDir, 'rss.xml'), feed.rss2(), 'utf8');
|
||||
// console.log('rss.xml:');
|
||||
// console.log(feed.rss2());
|
||||
await fs.writeFile(join(site.outDir, 'rss.xml'), feed.rss2());
|
||||
},
|
||||
|
||||
head: [
|
||||
@@ -118,6 +149,9 @@ export default defineConfig({
|
||||
md.use(footnote);
|
||||
md.use(groupIconMdPlugin);
|
||||
},
|
||||
languageAlias: {
|
||||
mjs: 'js',
|
||||
},
|
||||
},
|
||||
|
||||
themeConfig: {
|
||||
@@ -129,8 +163,7 @@ export default defineConfig({
|
||||
|
||||
footer: {
|
||||
message: [
|
||||
'<a class="light-netlify" href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-bg.svg" alt="Deploys by Netlify" style="display: inline;" /></a>',
|
||||
'<a class="dark-netlify" href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-accent.svg" alt="Deploys by Netlify" style="display: inline;" /></a>',
|
||||
'<span class="netlify-badge"><a class="light-netlify" href="https://www.netlify.com"><img src="/netlify-badge-light.svg" alt="Deploys by Netlify" /></a><a class="dark-netlify" href="https://www.netlify.com"><img src="/netlify-badge-dark.svg" alt="Deploys by Netlify" /></a></span>',
|
||||
'Released under the <a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE">MIT License</a>.',
|
||||
].join('<br/>'),
|
||||
copyright:
|
||||
@@ -205,6 +238,7 @@ export default defineConfig({
|
||||
menuItem('Storage', 'storage.md'),
|
||||
menuItem('Messaging', 'messaging.md'),
|
||||
menuItem('I18n', 'i18n.md'),
|
||||
menuItem('Favicons', 'favicons.md'),
|
||||
menuItem('Scripting', 'scripting.md'),
|
||||
menuItem('WXT Modules', 'wxt-modules.md'),
|
||||
menuItem('Frontend Frameworks', 'frontend-frameworks.md'),
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { createContentLoader } from 'vitepress';
|
||||
import { type ContentData, createContentLoader } from 'vitepress';
|
||||
import { PostFrontmatter } from '../utils/types';
|
||||
|
||||
export default createContentLoader('blog/*.md');
|
||||
declare const data: Array<ContentData & { frontmatter: PostFrontmatter }>;
|
||||
export { data };
|
||||
|
||||
export default createContentLoader<
|
||||
Array<ContentData & { frontmatter: PostFrontmatter }>
|
||||
>('blog/*.md');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolve } from 'node:path';
|
||||
import consola from 'consola';
|
||||
import spawn from 'nano-spawn';
|
||||
import { resolve } from 'node:path';
|
||||
import { version } from '../../../packages/wxt/package.json';
|
||||
|
||||
const cliDir = resolve('packages/wxt/src/cli/commands');
|
||||
const cliDirGlob = resolve(cliDir, '**');
|
||||
@@ -38,24 +39,20 @@ export default {
|
||||
|
||||
async function getHelp(command: string): Promise<string> {
|
||||
const args = command.split(' ');
|
||||
const res = await spawn(args[0], [...args.slice(1), '--help'], {
|
||||
const result = await spawn(args[0], [...args.slice(1), '--help'], {
|
||||
cwd: 'packages/wxt',
|
||||
});
|
||||
return res.stdout;
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
function getWxtHelp(command: string): Promise<string> {
|
||||
return getHelp(`pnpm -s wxt ${command}`.trim());
|
||||
async function getWxtHelp(command: string): Promise<string> {
|
||||
const res = await getHelp(`bun run --silent wxt ${command}`.trim());
|
||||
return res.replaceAll('{{version}}', version);
|
||||
}
|
||||
|
||||
async function getPublishExtensionHelp(command: string): Promise<string> {
|
||||
const res = await getHelp(
|
||||
`./node_modules/.bin/publish-extension ${command}`.trim(),
|
||||
`bun run --silent publish-extension ${command}`.trim(),
|
||||
);
|
||||
return res.replace(/\$ publish-extension/g, '$ wxt submit');
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
name: string;
|
||||
docs: string;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
|
||||
/* Colors */
|
||||
:root {
|
||||
--wxt-c-green-1: #0b8a00;
|
||||
--wxt-c-green-2: #096600;
|
||||
--wxt-c-green-3: #096600;
|
||||
--wxt-c-green-soft: rgba(11, 138, 0, 0.14);
|
||||
|
||||
--vp-c-brand-1: var(--wxt-c-green-1);
|
||||
--vp-c-brand-2: var(--wxt-c-green-2);
|
||||
--vp-c-brand-3: var(--wxt-c-green-3);
|
||||
--vp-c-brand-soft: var(--wxt-c-green-soft);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -13,56 +19,65 @@
|
||||
--wxt-c-green-soft: rgba(103, 212, 94, 0.14);
|
||||
}
|
||||
|
||||
/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
|
||||
|
||||
:root {
|
||||
--vp-c-brand-1: var(--wxt-c-green-1);
|
||||
--vp-c-brand-2: var(--wxt-c-green-2);
|
||||
--vp-c-brand-3: var(--wxt-c-green-3);
|
||||
--vp-c-brand-soft: var(--wxt-c-green-soft);
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Customize Individual Components */
|
||||
.vp-doc .no-vertical-dividers {
|
||||
th,
|
||||
td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.vp-doc .no-vertical-dividers th,
|
||||
.vp-doc .no-vertical-dividers td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.vp-doc .no-vertical-dividers tr {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
tr {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.VPSidebar {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.VPSidebarItem .badge {
|
||||
display: inline-block;
|
||||
min-width: 1.6em;
|
||||
padding: 0.3em 0.4em 0.2em;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75em;
|
||||
line-height: 1;
|
||||
margin-left: 0.5rem;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
background-color: var(--vp-c-default-2);
|
||||
}
|
||||
|
||||
.light-netlify {
|
||||
display: inline;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.dark .light-netlify {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dark-netlify {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dark .dark-netlify {
|
||||
display: inline;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* Add Aaron's favorite pic from Artemis II to the hero section because it's cool. */
|
||||
.dark .VPHome::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-image: url('../../assets/art002e009298.jpg');
|
||||
opacity: 50%;
|
||||
width: 100%;
|
||||
height: 110vh;
|
||||
background-size: cover;
|
||||
background-position: 90%;
|
||||
}
|
||||
|
||||
.dark .VPHome::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 10vh;
|
||||
top: 100vh;
|
||||
background: linear-gradient(to bottom, transparent, var(--vp-c-bg));
|
||||
}
|
||||
|
||||
.item {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -6,15 +6,19 @@ import ExampleSearch from '../components/ExampleSearch.vue';
|
||||
import BlogLayout from '../components/BlogLayout.vue';
|
||||
import './custom.css';
|
||||
import 'virtual:group-icons.css';
|
||||
import type { EnhanceAppContext } from 'vitepress/client';
|
||||
import { setupCodeGroupSync } from '../utils/code-groups-sync';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp(ctx) {
|
||||
enhanceApp(ctx: EnhanceAppContext) {
|
||||
ctx.app
|
||||
.component('Icon', Icon)
|
||||
.component('EntrypointPatterns', EntrypointPatterns)
|
||||
.component('UsingWxtSection', UsingWxtSection)
|
||||
.component('ExampleSearch', ExampleSearch)
|
||||
.component('blog', BlogLayout);
|
||||
|
||||
setupCodeGroupSync();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
const CODE_GROUP_SELECTORS = {
|
||||
root: '.vp-code-group',
|
||||
input: '.vp-code-group .tabs input',
|
||||
label: '.tabs label',
|
||||
} as const;
|
||||
|
||||
function getTabTitle(label: HTMLLabelElement) {
|
||||
return label.dataset.title?.trim() || label.textContent?.trim();
|
||||
}
|
||||
|
||||
function getCodeGroupTitle(input: HTMLInputElement) {
|
||||
const label = input.parentElement?.querySelector<HTMLLabelElement>(
|
||||
`label[for="${input.id}"]`,
|
||||
);
|
||||
|
||||
return label ? getTabTitle(label) : undefined;
|
||||
}
|
||||
|
||||
function findCodeGroupTabIndex(group: HTMLElement, title: string) {
|
||||
return Array.from(
|
||||
group.querySelectorAll<HTMLLabelElement>(CODE_GROUP_SELECTORS.label),
|
||||
).findIndex((label) => getTabTitle(label) === title);
|
||||
}
|
||||
|
||||
function syncCodeGroupSelection(group: HTMLElement, title: string) {
|
||||
const targetIndex = findCodeGroupTabIndex(group, title);
|
||||
|
||||
if (targetIndex < 0) return;
|
||||
|
||||
const blocks = group.querySelector('.blocks');
|
||||
const targetBlock = blocks?.children[targetIndex];
|
||||
if (!blocks || !targetBlock || targetBlock.classList.contains('active'))
|
||||
return;
|
||||
|
||||
blocks.querySelector('.active')?.classList.remove('active');
|
||||
targetBlock.classList.add('active');
|
||||
|
||||
const input = group.querySelectorAll<HTMLInputElement>(
|
||||
CODE_GROUP_SELECTORS.input,
|
||||
)[targetIndex];
|
||||
if (input) input.checked = true;
|
||||
}
|
||||
|
||||
function syncCodeGroupTabs(event: Event) {
|
||||
const target = event.target;
|
||||
if (
|
||||
!(target instanceof HTMLInputElement) ||
|
||||
!target.matches(CODE_GROUP_SELECTORS.input)
|
||||
)
|
||||
return;
|
||||
|
||||
const activeGroup = target.closest(CODE_GROUP_SELECTORS.root);
|
||||
const activeTitle = getCodeGroupTitle(target);
|
||||
if (!activeTitle) return;
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLElement>(CODE_GROUP_SELECTORS.root)
|
||||
.forEach((group) => {
|
||||
if (group === activeGroup) return;
|
||||
|
||||
syncCodeGroupSelection(group, activeTitle);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps every code group on the page in sync: selecting a package manager (or
|
||||
* any tab) in one group switches the matching tab in all the other groups.
|
||||
*/
|
||||
export function setupCodeGroupSync() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
window.addEventListener('change', syncCodeGroupTabs, true);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HeadConfig } from 'vitepress/types/shared';
|
||||
import type { HeadConfig } from 'vitepress';
|
||||
|
||||
export function meta(
|
||||
property: string,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { DefaultTheme } from 'vitepress';
|
||||
|
||||
type SidebarItem = DefaultTheme.SidebarItem;
|
||||
type NavItem = DefaultTheme.NavItem;
|
||||
type NavItemWithLink = DefaultTheme.NavItemWithLink;
|
||||
type NavItemWithChildren = DefaultTheme.NavItemWithChildren;
|
||||
type NavItemChildren = DefaultTheme.NavItemChildren;
|
||||
|
||||
@@ -19,7 +17,7 @@ export function navItem(text: string, arg2?: unknown): any {
|
||||
}
|
||||
|
||||
export function menuRoot(items: SidebarItem[]) {
|
||||
return items.map((item, index) => {
|
||||
return items.map((item) => {
|
||||
// item.collapsed = false; // uncomment to expand all level-0 items
|
||||
return item;
|
||||
});
|
||||
@@ -60,12 +58,6 @@ export function menuGroup(
|
||||
throw Error('Unknown overload');
|
||||
}
|
||||
|
||||
export function menuItems(items: SidebarItem[]) {
|
||||
return {
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
export function menuItem(
|
||||
text: string,
|
||||
link: string,
|
||||
@@ -77,9 +69,7 @@ export function menuItem(
|
||||
return { text, link };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up and add badges to typedoc leaf sections
|
||||
*/
|
||||
/** Clean up and add badges to typedoc leaf sections */
|
||||
export function prepareTypedocSidebar(items: SidebarItem[]) {
|
||||
// skip contents file
|
||||
const filtered = items.slice(1);
|
||||
@@ -99,9 +89,7 @@ export function prepareTypedocSidebar(items: SidebarItem[]) {
|
||||
}
|
||||
};
|
||||
|
||||
// process
|
||||
prepareItems(filtered);
|
||||
|
||||
// return
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ContentData } from 'vitepress';
|
||||
|
||||
export interface Example {
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -16,3 +18,15 @@ export type ExamplesMetadata = {
|
||||
};
|
||||
|
||||
export type KeySelectedObject = Record<string, boolean | undefined>;
|
||||
|
||||
export interface PostFrontmatter {
|
||||
title: string;
|
||||
description?: string;
|
||||
date: Date;
|
||||
authors: { name: string; github: string }[];
|
||||
}
|
||||
|
||||
export interface Post
|
||||
extends Omit<ContentData, 'frontmatter'>, Omit<PostFrontmatter, 'date'> {
|
||||
date: Date;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 237 KiB |
@@ -0,0 +1,520 @@
|
||||
# Add extension entries to end of the list. On the website, extensions will be sorted by a combination of users and rating.
|
||||
#
|
||||
# Change the commit message or PR title to:
|
||||
#
|
||||
# > "docs: Added "[extension name]" to showcase"
|
||||
#
|
||||
# Use:
|
||||
#
|
||||
# - `chromeId` for the Chrome Web Store listing,
|
||||
# - `firefoxSlug` for Firefox Add-ons (slug from https://addons.mozilla.org/firefox/addon/{slug}/),
|
||||
# - `edgeId` for the Edge Add-ons listing (ID from https://microsoftedge.microsoft.com/addons/detail/{slug}/{id})
|
||||
#
|
||||
# You may include one or more of these fields for your extension.
|
||||
|
||||
- # Anylang Translator
|
||||
chromeId: injdenmpjncknciccmjjmjlgbmnkince
|
||||
firefoxSlug: anylang-translator
|
||||
|
||||
- # GitHub: Better Line Counts
|
||||
chromeId: ocfdgncpifmegplaglcnglhioflaimkd
|
||||
firefoxSlug: github-better-line-counts
|
||||
|
||||
- # Anime Skip Player
|
||||
chromeId: mgmdkjcljneegjfajchedjpdhbadklcf
|
||||
firefoxSlug: anime-skip
|
||||
|
||||
- # UltraWideo
|
||||
chromeId: bfbnagnphiehemkdgmmficmjfddgfhpl
|
||||
firefoxSlug: ultrawideo
|
||||
|
||||
- # StayFree - Website Blocker & Web Analytics
|
||||
chromeId: elfaihghhjjoknimpccccmkioofjjfkf
|
||||
firefoxSlug: stayfree
|
||||
edgeId: aghmglalnfmjbipodadgbgpakneabgnp
|
||||
|
||||
- # Doozy: Ai Made Easy
|
||||
chromeId: okifoaikfmpfcamplcfjkpdnhfodpkil
|
||||
|
||||
- # tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
chromeId: lknmjhcajhfbbglglccadlfdjbaiifig
|
||||
|
||||
- # Youtube中文配音
|
||||
chromeId: oglffgiaiekgeicdgkdlnlkhliajdlja
|
||||
|
||||
- # PreMiD
|
||||
chromeId: agjnjboanicjcpenljmaaigopkgdnihi
|
||||
|
||||
- # Markdown Sticky Notes
|
||||
chromeId: aiakblgmlabokilgljkglggnpflljdgp
|
||||
firefoxSlug: markdown-sticky-notes
|
||||
|
||||
- # DocVersionRedirector
|
||||
chromeId: nomnkbngkijpffepcgbbofhcnafpkiep
|
||||
|
||||
- # Plex Skipper
|
||||
chromeId: ceicccfeikoipigeghddpocceifjelph
|
||||
firefoxSlug: plex-skipper
|
||||
|
||||
- # GitHub Custom Notifier
|
||||
chromeId: aelkipgppclpfimeamgmlonimflbhlgf
|
||||
firefoxSlug: github-custom-notifier
|
||||
|
||||
- # Fluent Read
|
||||
chromeId: djnlaiohfaaifbibleebjggkghlmcpcj
|
||||
|
||||
- # Facebook Video Controls
|
||||
chromeId: nhclljcpfmmaiojbhhnkpjcfmacfcian
|
||||
firefoxSlug: facebook-video-controls
|
||||
|
||||
- # ElemSnap - Quick capture of webpage elements and conversion to images,
|
||||
chromeId: mblkhbaakhbhiimkbcnmeciblfhmafna
|
||||
|
||||
- # MS Edge TTS (Text to Speech)
|
||||
chromeId: oajalfneblkfiejoadecnmodfpnaeblh
|
||||
firefoxSlug: ms-edge-tts-text-to-speech
|
||||
|
||||
- # YTBlock - Block any content from YouTube™
|
||||
chromeId: nedcanggplmbbgmlpcjiafgjcpdimpea
|
||||
|
||||
- # demo.fun - Interactive product demos that convert
|
||||
chromeId: oadbjpccljkplmhnjekgjamejnbadlne
|
||||
|
||||
- # SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
|
||||
chromeId: iopdafdcollfgaoffingmahpffckmjni
|
||||
|
||||
- # WorkFlowy MultiFlow
|
||||
chromeId: khjdmjcmpolknpccmaaipmidphjokhdf
|
||||
|
||||
- # 香草布丁🌿🍮- https://github.com/Xdy1579883916/vanilla-pudding
|
||||
chromeId: fencadnndhdeggodopebjgdfdlhcimfk
|
||||
|
||||
- # MaxFocus: Link Preview
|
||||
chromeId: bnacincmbaknlbegecpioobkfgejlojp
|
||||
firefoxSlug: maxfocus-link-preview
|
||||
|
||||
- # 汇率转换-中文版本
|
||||
chromeId: bcpgdpedphodjcjlminjbdeejccjbimp
|
||||
|
||||
- # Currency Converter Plus
|
||||
chromeId: loeilaonggnalkaiiaepbegccilkmjjp
|
||||
firefoxSlug: currency-converter-plus
|
||||
|
||||
- # Respond Easy
|
||||
chromeId: npcnninnjghigjfiecefheeibomjpkak
|
||||
|
||||
- # mindful - stay focused on your goals
|
||||
chromeId: cfkdcideecefncbglkhneoflfnmhoicc
|
||||
|
||||
- # 1Proompt
|
||||
chromeId: lnhejcpclabmbgpiiomjbhalblnnbffg
|
||||
|
||||
- # NiceTab - https://github.com/web-dahuyou/NiceTab
|
||||
chromeId: fonflmjnjbkigocpoommgmhljdpljain
|
||||
firefoxSlug: nice-tab-manager
|
||||
edgeId: ompjiaelpibiggcnanhbdblkhfdmkgnl
|
||||
|
||||
- # Draftly for LinkedIn
|
||||
chromeId: fcffekbnfcfdemeekijbbmgmkognnmkd
|
||||
|
||||
- # YouTube Summarized - Summarize any YouTube video
|
||||
chromeId: nkndldfehcidpejfkokbeghpnlbppdmo
|
||||
|
||||
- # 社媒助手 - https://github.com/iszhouhua/social-media-copilot
|
||||
chromeId: dbichmdlbjdeplpkhcejgkakobjbjalc
|
||||
|
||||
- # Dofollow Links for SEO
|
||||
chromeId: opepfpjeogkbgeigkbepobceinnfmjdd
|
||||
|
||||
- # ChatGPT Writer: Use AI on Any Site (GPT-4o, Claude, Gemini, and More)
|
||||
chromeId: pdnenlnelpdomajfejgapbdpmjkfpjkp
|
||||
|
||||
- # discord message translator
|
||||
chromeId: jobnhifpphkgoelnhnopgkdhbdkiadmj
|
||||
|
||||
- # Habit Tracker app widget for daily habit tracking
|
||||
chromeId: ncokhechhpjgjonhjnlaneglmdkfkcbj
|
||||
|
||||
- # Catppuccin for GitHub File Explorer Icons
|
||||
chromeId: lnjaiaapbakfhlbjenjkhffcdpoompki
|
||||
firefoxSlug: catppuccin-web-file-icons
|
||||
|
||||
- # WebChat: Chat with anyone on any website
|
||||
chromeId: cpaedhbidlpnbdfegakhiamfpndhjpgf
|
||||
|
||||
- # YouTube Auto HD + FPS
|
||||
chromeId: fcphghnknhkimeagdglkljinmpbagone
|
||||
firefoxSlug: youtube-auto-hd-fps
|
||||
|
||||
- # YouTube Auto Feed
|
||||
chromeId: jcdebdlnakhdinkindpogcehnhggbfad
|
||||
firefoxSlug: youtube-auto-feed
|
||||
|
||||
- # YouTube Like-Dislike Shortcut
|
||||
chromeId: fdkpkpelkkdkjhpacficichkfifijipc
|
||||
firefoxSlug: youtube-like-dislike-shortcut
|
||||
|
||||
- # Twitch Channel Points Bonus Collector
|
||||
chromeId: kbbdnbaghpcjpdhbjbccadodjejlkkgg
|
||||
firefoxSlug: twitch-cp-bonus-collector
|
||||
|
||||
- # Skillshare Player Control
|
||||
chromeId: agbhgcomfpcfboebbfmefbicfkpnlfeg
|
||||
firefoxSlug: skillshare-player-control
|
||||
|
||||
- # MultiViewer Companion
|
||||
chromeId: lpomjgbicdemjkgmbnkjncgdebogkhlb
|
||||
firefoxSlug: multiviewer-companion
|
||||
|
||||
- # Sync Watch - Watch videos together on any site
|
||||
chromeId: ggiafipgeeaaahnjamgpjcgkdpanhddg
|
||||
firefoxSlug: syncwatch
|
||||
|
||||
- # Keyword Rank Checker
|
||||
chromeId: nmldnjcblcihmegipecakhmnieiofmgl
|
||||
firefoxSlug: keyword-rank-checker-sa
|
||||
|
||||
- # YouTube Simple View - Hide distractions & more
|
||||
chromeId: gppllamhaciichleihemgilcpledblpn
|
||||
firefoxSlug: youtube-simple-view
|
||||
|
||||
- # Propbar - Property Data Enhancer
|
||||
chromeId: pccbghdfdnnkkbcdcibchpbffdgednkf
|
||||
|
||||
- # Text Search Pro - Search by case and whole-word match!
|
||||
chromeId: lfknakglefggmdkjdfhhofkjnnolffkh
|
||||
firefoxSlug: text-search-pro-ext
|
||||
|
||||
- # Invoice Generator
|
||||
chromeId: mbenhbocjckkbaojacmaepiameldglij
|
||||
firefoxSlug: quick-invoice-generator
|
||||
|
||||
- # Monthly Bill Tracker
|
||||
chromeId: phlfhkmdofajnbhgmbmjkbkdgppgoppb
|
||||
|
||||
- # Wandpen - Instantly improve your writing with AI
|
||||
chromeId: macmkmchfoclhpbncclinhjflmdkaoom
|
||||
|
||||
- # YouTube Hider - Remove Comments By Keywords, Usernames & Tools
|
||||
chromeId: lhmgechokhmdekdpgkkemoeecelcaonm
|
||||
|
||||
- # QA Compass - Record standardized bug reports easily
|
||||
chromeId: imgheieooppmahcgniieddodaliodeeg
|
||||
|
||||
- # aesthetic Notion, styled
|
||||
chromeId: npgghjedpchajflknnbngajkjkdhncdo
|
||||
|
||||
- # Eye Dropper
|
||||
chromeId: hmdcmlfkchdmnmnmheododdhjedfccka
|
||||
firefoxSlug: eye_dropper
|
||||
|
||||
- # Cursorful - Screen Recorder with Auto Zoom
|
||||
chromeId: eihpmapodnppeemkhkbhikmggfojdkjd
|
||||
|
||||
- # Show IP – Live View of Website IPs for Developers
|
||||
chromeId: hjjkgbibknbahijglkffklflidncplkn
|
||||
|
||||
- # Strong Password Generator
|
||||
chromeId: ilbikcehnpkmldojkcmlldkoelofnbde
|
||||
firefoxSlug: strongpasswordgenerator
|
||||
|
||||
- # ZenGram: Mindful Instagram, Your Way
|
||||
chromeId: ocllfkhcdopiafndigclebelbecaiocp
|
||||
|
||||
- # Blync: Preview Links, Selection Search, AI Assistant
|
||||
chromeId: odffpjnpocjfcaclnenaaaddghkgijdb
|
||||
|
||||
- # HTML to Markdown - Convert webpages to markdown
|
||||
chromeId: kofbbilhmnkcmibjbioafflgmpkbnmme
|
||||
|
||||
- # Walmart WFS Profit Calculator
|
||||
chromeId: boecmgggeigllcdocgioijmleimjbfkg
|
||||
firefoxSlug: walmart-wfs-profit-calculator
|
||||
|
||||
- # Youtube Live Chat Fullscreen
|
||||
chromeId: dlnjcbkmomenmieechnmgglgcljhoepd
|
||||
firefoxSlug: youtube-live-chat-fullscreen
|
||||
|
||||
- # Python Code Runner
|
||||
chromeId: keiealdacakpnbbljlmhfgcebmaadieg
|
||||
firefoxSlug: code-runner-manager
|
||||
|
||||
- # Monochromate
|
||||
chromeId: hafcajcllbjnoolpfngclfmmgpikdhlm
|
||||
firefoxSlug: monochromate
|
||||
|
||||
- # AliasVault - Open-Source Password & (Email) Alias Manager
|
||||
chromeId: bmoggiinmnodjphdjnmpcnlleamkfedj
|
||||
|
||||
- # SnapThePrice: AI-Powered Real-time Lowest Price Finder
|
||||
chromeId: hlnhhamckimoaiekbglafiebkfimhapb
|
||||
|
||||
- # radiofrance - news & broadcasts (French), music (international)
|
||||
chromeId: gdjampjdgjmbifnhldgcnccdjkcoicmg
|
||||
|
||||
- # Blens - Time Tracker and AI Insight
|
||||
chromeId: jlnhphlghikichhgbnkepenehbmloenb
|
||||
firefoxSlug: blens-timetracker-ai-insight
|
||||
|
||||
- # Always Light Mode - Setting website always in light mode
|
||||
chromeId: njnammmpdodmfkodnfpammnpdcbhnlcm
|
||||
|
||||
- # DesignPicker - Color Picker & Font Detector
|
||||
chromeId: lblmfclcfniabobmamfkdogcgdagbhhb
|
||||
firefoxSlug: design-colorpicker-fontdetect
|
||||
|
||||
- # Web to PDF
|
||||
chromeId: pamnlaoeobcmhkliljfaofekeddpmfoh
|
||||
firefoxSlug: export-web-to-pdf
|
||||
|
||||
- # Online CSV Viewer
|
||||
chromeId: jmbcbeepjfenihlocplnbmbhimcoooka
|
||||
firefoxSlug: csv-viewer
|
||||
|
||||
- # YouTube Video Transcript
|
||||
chromeId: nkjcoophmpcmmgadnljnlpbpfdfacgbo
|
||||
firefoxSlug: youtube-transcript-copy
|
||||
|
||||
- # NetSuite Record Scripts
|
||||
chromeId: lcaieahkjgeggeiihblhcjbbjlppgieh
|
||||
firefoxSlug: netsuite-scripts-manager
|
||||
|
||||
- # VueTracker
|
||||
chromeId: gmocfknjllodfiomnljmaehcplnekhlo
|
||||
|
||||
- # CanCopy - A web extension that allow you to copy any content from website
|
||||
chromeId: ggcfemmoabhhelfkhknhbnkmeahloiod
|
||||
|
||||
- # Read Frog
|
||||
chromeId: modkelfkcfjpgbfmnbnllalkiogfofhb
|
||||
firefoxSlug: read-frog-open-ai-translator
|
||||
|
||||
- # Bilibili Feed History Helper
|
||||
chromeId: npfopljnjbamegincfjelhjhnonnjloo
|
||||
|
||||
- # NZBDonkey - The ultimate NZB file download tool
|
||||
chromeId: edkhpdceeinkcacjdgebjehipmnbomce
|
||||
|
||||
- # WeChat Markdown Editor(微信 Markdown 编辑器)
|
||||
chromeId: cckggnbnimdbbpmdinkkgbbncopbloob
|
||||
|
||||
- # Tab Grab
|
||||
chromeId: jcblcjolcojmfopefcighfmkkefbaofg
|
||||
firefoxSlug: tab-grab
|
||||
|
||||
- # BrowserLens - https://browserlens.com/
|
||||
chromeId: eehmoikadcijkapfjocnhjclpbaindlb
|
||||
firefoxSlug: browserlens
|
||||
|
||||
- # Epic Games Library Extension
|
||||
chromeId: hfhellofkjebbchcdffmicekjdomkcmc
|
||||
firefoxSlug: epic-games-library-extension
|
||||
|
||||
- # Zen Analytics Pixel Tracker - zapt.web.app
|
||||
chromeId: gknigcbhlammoakmmdddkblknanpjiac
|
||||
|
||||
- # Crypto Pulse - Compose your newtab with nature images, widgets & realtime Crypto Price & Bitcoin RSS.
|
||||
chromeId: cnklededohhcbmjjdlbjdkkihkgoggol
|
||||
|
||||
- # Youtube Video Scheduler
|
||||
chromeId: miponnamafdenpgjemkknimgjfibicdc
|
||||
firefoxSlug: youtube-video-scheduler
|
||||
|
||||
- # Chatslator: Livestream Chat Translator
|
||||
chromeId: nhmbcmalgpkjbomhlhgdicanmkkaajmg
|
||||
firefoxSlug: chatslator
|
||||
|
||||
- # 公众号阅读增强器 - https://wxreader.honwhy.wang
|
||||
chromeId: mbamjfdjbcdgpopfnkkmlohadbbnplhm
|
||||
|
||||
- # 토탐정
|
||||
chromeId: hannhecbnjnnbbafffmogdlnajpcomek
|
||||
firefoxSlug: 토탐정
|
||||
|
||||
- # 2FAS Pass - https://2fas.com/
|
||||
chromeId: ehboaofjncodknjkngdggmpdinhdoijp
|
||||
firefoxSlug: 2fas-pass-browser-extension
|
||||
|
||||
- # Quick Prompt - https://github.com/wenyuanw/quick-prompt
|
||||
chromeId: hnjamiaoicaepbkhdoknhhcedjdocpkd
|
||||
firefoxSlug: quick-prompt
|
||||
|
||||
- # Add QR Code Generator Icon Back To Address Bar
|
||||
chromeId: kacblhilkacgfnkjfodalohcnllcgmjd
|
||||
|
||||
- # Piwik PRO Tracking Helper
|
||||
chromeId: fkbdlogfdjmpfepbbbjcgcfbgbcfcnne
|
||||
firefoxSlug: piwik-pro-tracking-helper
|
||||
|
||||
- # PIPX - Take Control of Picture-in-Picture, Automatically
|
||||
chromeId: nkbikckldmljjiiajklecmgmajgapbfl
|
||||
|
||||
- # Browsely - AI-powered browser extension
|
||||
chromeId: hgppdobcpkfkmiegekaglonjajeojmdd
|
||||
|
||||
- # Filmbudd Pro - Simple, private – and synced ratings and watch notes across all your devices
|
||||
chromeId: ehmoihnjgkdimihkhokkmfjdgomohjgm
|
||||
|
||||
- # MultiField CopyCat - Copy, Paste & Autofill Web Forms Instantly
|
||||
chromeId: alglchohmdikgdjhafiicilegegieafa
|
||||
|
||||
- # ChatSight - Add Table of Contents to ChatGPT
|
||||
chromeId: aamihahiiogceidpbnfgehacgiecephe
|
||||
|
||||
- # BetterCampus (prev. BetterCanvas)
|
||||
chromeId: cndibmoanboadcifjkjbdpjgfedanolh
|
||||
firefoxSlug: better-canvas
|
||||
|
||||
- # Leetcode Fonts - Change fonts in leetcode effortlessly
|
||||
chromeId: hinfimgacobnellbncbcpdlpaapcofaa
|
||||
|
||||
- # TranslateManga - Manga Translator & Manga Tracker
|
||||
chromeId: kbkbfefhhabpkibojinapkkgciiacggg
|
||||
|
||||
- # SiteData - Free Website Traffic Checker & Reverse AdSense Tool
|
||||
chromeId: emeakbgdecgmdjgegnejpppcnkcnoaen
|
||||
firefoxSlug: sitedata
|
||||
|
||||
- # Livestream Chat Reader - Text-to-Speech for YouTube/Twitch chat
|
||||
chromeId: gpnckbhgpnjciklpoehkmligeaebigaa
|
||||
|
||||
- # ChatGPT Token Counter - Count tokens in real time on chatgpt conversation
|
||||
chromeId: fjlalaedpfcojcfpkgkglbjjbbkofgnl
|
||||
firefoxSlug: chatgpt-token-counter
|
||||
|
||||
- # LinuxDo Scripts - 为 linux.do 用户提供了一些增强功能
|
||||
chromeId: fbgblmjbeebanackldpbmpacppflgmlj
|
||||
firefoxSlug: linux_do-scripts
|
||||
|
||||
- # Zen Virtual Piano - https://zen-piano.web.app/
|
||||
chromeId: dfacnjidgbagicaekenjgclfnhdnjjdi
|
||||
|
||||
- # Crypto Pulse price tracker - https://get-crypto-pulse.web.app/
|
||||
chromeId: naeibcegmgpofimedkmfgjgphfhfhlab
|
||||
|
||||
- # Redirect Web - Automatically redirect pages or open them in another app
|
||||
chromeId: ffglckbhfbfmdkefdmjbhpnffkcmlhdh
|
||||
|
||||
- # Capture It - Capture & Edit Screenshots
|
||||
chromeId: eglpfhbhmelampoihamjomgkeobgdofl
|
||||
|
||||
- # Teams Chat Exporter
|
||||
chromeId: jmghclbfbbapimhbgnpffbimphlpolnm
|
||||
firefoxSlug: teams-chat-exporter
|
||||
|
||||
- # Lofi BGM Player - Free lofi focus music for work & study
|
||||
chromeId: jdcppdokgfbnhiacbeplahgnciahnhck
|
||||
firefoxSlug: lofi-bgm-player
|
||||
|
||||
- # Margin - Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.
|
||||
chromeId: cgpmbiiagnehkikhcbnhiagfomajncpa
|
||||
|
||||
- # KeyFloat - Floating multilingual keyboard with native key mappings, drag, dark mode, sounds, and dynamic layouts for macOS & Windows
|
||||
chromeId: mfjdonmgmgcijagclnkfhmjiblbfjaid
|
||||
|
||||
- # Glossy New Tab - Say Goodbye to Boring Tabs with live wallpapers
|
||||
chromeId: dhiekgdaipindoapjmcnpompdknjeijf
|
||||
firefoxSlug: glossy-new-tab
|
||||
|
||||
- # All API Hub – AI Relay & New API Manager - https://github.com/qixing-jk/all-api-hub
|
||||
chromeId: lapnciffpekdengooeolaienkeoilfeo
|
||||
firefoxSlug: all-api-hub
|
||||
|
||||
- # Scrape Similar - Extract data from websites into spreadsheets - https://github.com/zizzfizzix/scrape-similar
|
||||
chromeId: bhgobenflkkhfcgkikejaaejenoddcmo
|
||||
|
||||
- # isTrust - https://github.com/Internet-Society-Belgium/isTrust/
|
||||
chromeId: kinlknncggaihnhdcalijdmpbhbflalm
|
||||
|
||||
- # Dymo
|
||||
chromeId: ojpakgiekphppgkcdihbjpafobhnhlkp
|
||||
|
||||
- # Extension Rank Checker - Extension Ranker
|
||||
chromeId: pmgehhllikbjmadpenhabejhpemplhmd
|
||||
firefoxSlug: rank-checker
|
||||
|
||||
- # PlayFaster - Enhanced playback speed control for online videos and audio
|
||||
chromeId: fppcbkhpahkbgijkdcpjgjmhpfbmfiih
|
||||
firefoxSlug: playfaster
|
||||
|
||||
- # LatTab - Learn Latin With Every New Tab
|
||||
chromeId: eiocjaocpmackhbaffoejkcmnfbdpgpj
|
||||
firefoxSlug: lattab-learn-latin-new-tabs
|
||||
|
||||
- # Vim What? - Interactive Vim command reference
|
||||
chromeId: ngbehgnlcdjkbnihgpkgdangbhemidge
|
||||
firefoxSlug: vim-what
|
||||
|
||||
- # FRED - Fraud Recognition Easy Detection
|
||||
chromeId: bjdbcabacnlmbpcmiapcdfancfgcakfn
|
||||
firefoxSlug: fred
|
||||
|
||||
- # Clear Wisdom - Gems of wisdom from James Clear's 3-2-1 newsletter
|
||||
chromeId: jijfmgoijddfmlcdghopbkdpelbpmjdm
|
||||
firefoxSlug: clear-wisdom
|
||||
|
||||
- # Soundcloud Enhanced Pro
|
||||
chromeId: ggplcohodggmdfpopelnpplhgfjclomi
|
||||
|
||||
- # AlarmBot: ULTIMATE Web Monitoring & Smart Price Alerts
|
||||
chromeId: mpckalcodookackleecihhnngdibelif
|
||||
|
||||
- # TF2 Trader - TF2 & Steam Trading Extension - https://github.com/offish/tf2-trader
|
||||
chromeId: gmicpekfpbikhibodgokfpghadkclhoe
|
||||
|
||||
- # Mimik
|
||||
chromeId: jmfohdaflahliammccpiadmkcibohgha
|
||||
firefoxSlug: mimik
|
||||
|
||||
- # Modrinth Extras - https://github.com/creeperkatze/modrinth-extras
|
||||
chromeId: ajmkilipadfpaefpcjfgnkejalmhdlcj
|
||||
firefoxSlug: modrinth-extras
|
||||
|
||||
- # Pi-hole In One - https://github.com/creeperkatze/pihole-in-one
|
||||
chromeId: gaaobidjebianpcngcfpkniaocibidhe
|
||||
firefoxSlug: pihole-in-one
|
||||
|
||||
- # QIE Wallet – Non-Custodial Multi-Chain Crypto Wallet
|
||||
chromeId: oljchdcgmibnjbbopolafbjncfhdacjb
|
||||
|
||||
- # Headstart – Supercharge your browsing efficiency https://useheadstart.app/
|
||||
chromeId: fhhldbfombjdnjnpeojjohloohmbanda
|
||||
|
||||
- # Redmine Time Tracking
|
||||
chromeId: ldcanhhkffokndenejhafhlkapflgcjg
|
||||
firefoxSlug: redmine-time-tracking
|
||||
|
||||
- # Telsia: Eye Strain Reduction
|
||||
chromeId: iakkmikmbknfcffhalijnhijblikoldc
|
||||
firefoxSlug: telsia-eye-strain-reduction
|
||||
|
||||
- # Sound Booster & Equalizer
|
||||
chromeId: podnfipggfpfcpglhjebcefinglohbhn
|
||||
|
||||
- # Link Later
|
||||
chromeId: gpehbbegbcpjmipphokcmfhkchhcpfam
|
||||
|
||||
- # Google Cloud Skills Boost - Helper
|
||||
chromeId: lmbhjioadhcoebhgapaidogodllonbgg
|
||||
firefoxSlug: cloud-skills-boost-helper
|
||||
|
||||
- # Property Track - https://www.propertytrack.co
|
||||
chromeId: ahknifalnhmbanoclebllcolcdomfdop
|
||||
firefoxSlug: propertytrack
|
||||
|
||||
- # WageSlav3
|
||||
chromeId: anhkcmapjhiiofpcgglfljphdgppilof
|
||||
|
||||
- # LinkedIn Games Solver
|
||||
chromeId: jnhgapnkejaijibcdhcldhdfikjmdaph
|
||||
firefoxSlug: linkedin-games-solver-addon
|
||||
|
||||
- # Hayami: Anime comments & discussions
|
||||
chromeId: nhkggpiaeaeeeimohfpchnjobbamfcbg
|
||||
firefoxSlug: hayami
|
||||
|
||||
- # CopyApes Assistant
|
||||
chromeId: affmjifigldmicnbgpghddaneomejmfo
|
||||
|
||||
- # Leetcode Enhancer
|
||||
chromeId: cpoclfijojgjiafnlgnhjalkaiabcjch
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 132 KiB |
@@ -70,3 +70,7 @@ You don't have to use `#imports` if you don't like - you can continue importing
|
||||
Happy Coding 😄
|
||||
|
||||
> P.S. Yes, this is exactly how [Nuxt's `#imports`](https://nuxt.com/docs/guide/concepts/auto-imports#explicit-imports) works! We use the exact same library, [`unimport`](https://github.com/unjs/unimport).
|
||||
|
||||
---
|
||||
|
||||
[Discuss this blog post on Github](https://github.com/wxt-dev/wxt/discussions/1543).
|
||||
|
||||
@@ -14,7 +14,7 @@ You can configure browser startup in 3 places:
|
||||
|
||||
1. `<rootDir>/web-ext.config.ts`: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers
|
||||
|
||||
```ts
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineWebExtConfig({
|
||||
@@ -22,7 +22,7 @@ You can configure browser startup in 3 places:
|
||||
});
|
||||
```
|
||||
|
||||
2. `<rootDir>/wxt.config.ts`: Via the [`runner` config](/api/reference/wxt/interfaces/InlineConfig#runner), included in version control
|
||||
2. `<rootDir>/wxt.config.ts`: Via the [`webExt` config](/api/reference/wxt/interfaces/InlineConfig#webext), included in version control
|
||||
3. `$HOME/web-ext.config.ts`: Provide default values for all WXT projects on your computer
|
||||
|
||||
## Recipes
|
||||
@@ -31,7 +31,9 @@ You can configure browser startup in 3 places:
|
||||
|
||||
To set or customize the browser opened during development:
|
||||
|
||||
```ts
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineWebExtConfig({
|
||||
binaries: {
|
||||
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
|
||||
@@ -41,6 +43,18 @@ export default defineWebExtConfig({
|
||||
});
|
||||
```
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
webExt: {
|
||||
binaries: {
|
||||
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
|
||||
firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
|
||||
edge: '/path/to/edge', // Open MS Edge when running "wxt -b edge"
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
By default, WXT will try to automatically discover where Chrome/Firefox are installed. However, if you have chrome installed in a non-standard location, you need to set it manually as shown above.
|
||||
|
||||
### Persist Data
|
||||
@@ -49,11 +63,13 @@ By default, to keep from modifying your browser's existing profiles, `web-ext` c
|
||||
|
||||
Right now, Chromium based browsers are the only browsers that support overriding this behavior and persisting data when running the `dev` script multiple times.
|
||||
|
||||
To persist data, set the `--user-data-dir` flag:
|
||||
To persist data, set the `--user-data-dir` flag in any of the config files mentioned above:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Mac/Linux]
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineWebExtConfig({
|
||||
chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],
|
||||
});
|
||||
@@ -61,6 +77,7 @@ export default defineWebExtConfig({
|
||||
|
||||
```ts [Windows]
|
||||
import { resolve } from 'node:path';
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineWebExtConfig({
|
||||
// On Windows, the path must be absolute
|
||||
@@ -81,8 +98,31 @@ You can use any directory you'd like for `--user-data-dir`, the examples above c
|
||||
|
||||
If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:
|
||||
|
||||
```ts
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineWebExtConfig({
|
||||
disabled: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Enabling Chrome Features
|
||||
|
||||
Some APIs are disabled in Chrome during development because of the default flags `web-ext` uses to launch Chrome, like the [Prompt API](https://developer.chrome.com/docs/ai/prompt-api).
|
||||
|
||||
If your extension depends on new features or services, you can enable them via `chromiumArgs`:
|
||||
|
||||
```ts
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineWebExtConfig({
|
||||
chromiumArgs: [
|
||||
// For example, this flag enables the Prompt API
|
||||
'--disable-features=DisableLoadExtensionCommandLineSwitch',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
There is no comprehensive list of what feature flags enable what APIs and services.
|
||||
|
||||
Alternatively, if you can't find a flag that enables a feature you're looking for, [disable the opening the browser during development](#disable-opening-browser) and use your regular chrome profile for development.
|
||||
|
||||
@@ -10,7 +10,7 @@ WXT does several pre-processing steps to try and prevent errors during this proc
|
||||
2. Use `@webext-core/fake-browser` to create a fake version of the `chrome` and `browser` globals expected by extensions.
|
||||
3. Pre-process the JS/TS code, stripping out the `main` function then tree-shaking unused code from the file
|
||||
|
||||
However, this process is not perfect. It doesn't setup all the globals found in the browser and the APIs may behave differently. As such, **_you should avoid using browser or extension APIs outside the `main` function of your entrypoints!_**
|
||||
However, this process is not perfect. It doesn't setup all the globals found in the browser and the APIs may behave differently. As such, **_you should avoid using browser or extension APIs outside the `main` function of your entrypoints!_** See [Entrypoint Limitations](/guide/essentials/extension-apis#entrypoint-limitations) for more details.
|
||||
|
||||
:::tip
|
||||
If you're running into errors while importing entrypoints, run `wxt prepare --debug` to see more details about this process. When debugging, WXT will print out the pre-processed code to help you identify issues.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
WXT supports [dotenv files the same way as Vite](https://vite.dev/guide/env-and-mode.html#env-files). Create any of the following files:
|
||||
|
||||
```
|
||||
```plaintext
|
||||
.env
|
||||
.env.local
|
||||
.env.[mode]
|
||||
@@ -42,6 +42,8 @@ WXT provides some custom environment variables based on the current command:
|
||||
| `import.meta.env.EDGE` | `boolean` | Equivalent to `import.meta.env.BROWSER === "edge"` |
|
||||
| `import.meta.env.OPERA` | `boolean` | Equivalent to `import.meta.env.BROWSER === "opera"` |
|
||||
|
||||
You can set the [`targetBrowsers`](/api/reference/wxt/interfaces/InlineConfig#targetbrowsers) option to make the `BROWSER` variable a more specific type, like `"chrome" | "firefox"`.
|
||||
|
||||
You can also access all of [Vite's environment variables](https://vite.dev/guide/env-and-mode.html#env-variables):
|
||||
|
||||
| Usage | Type | Description |
|
||||
@@ -63,7 +65,6 @@ To use environment variables in the manifest, you need to use the function synta
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
extensionApi: 'chrome',
|
||||
modules: ['@wxt-dev/module-vue'],
|
||||
manifest: { // [!code --]
|
||||
oauth2: { // [!code --]
|
||||
@@ -79,3 +80,16 @@ export default defineConfig({
|
||||
```
|
||||
|
||||
WXT can't load your `.env` files until after the config file has been loaded. So by using the function syntax for `manifest`, it defers creating the object until after the `.env` files are loaded into the process.
|
||||
|
||||
Note that Vite's runtime environment variables, like `import.meta.env.DEV`, will not be defined. Instead, access the `mode` like this:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: ({ mode }) => {
|
||||
const isDev = mode === 'development';
|
||||
console.log('Is development mode:', isDev);
|
||||
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -11,7 +11,7 @@ export default defineConfig({
|
||||
hooks: {
|
||||
'build:manifestGenerated': (wxt, manifest) => {
|
||||
if (wxt.config.mode === 'development') {
|
||||
manifest.title += ' (DEV)';
|
||||
manifest.name += ' (DEV)';
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -20,6 +20,8 @@ export default defineConfig({
|
||||
|
||||
Most hooks provide the `wxt` object as the first argument. It contains the resolved config and other info about the current build. The other arguments can be modified by reference to change different parts of the build system.
|
||||
|
||||
You can find the [list of all available hooks](/api/reference/wxt/interfaces/WxtHooks) in the API reference.
|
||||
|
||||
Putting one-off hooks like this in your config file is simple, but if you find yourself writing lots of hooks, you should extract them into [WXT Modules](/guide/essentials/wxt-modules) instead.
|
||||
|
||||
## Execution Order
|
||||
@@ -32,7 +34,7 @@ Because hooks can be defined in multiple places, including [WXT Modules](/guide/
|
||||
|
||||
To see the order for your project, run `wxt prepare --debug` flag and search for the "Hook execution order":
|
||||
|
||||
```
|
||||
```plaintext
|
||||
⚙ Hook execution order:
|
||||
⚙ 1. wxt:built-in:unimport
|
||||
⚙ 2. src/modules/auto-icons.ts
|
||||
@@ -50,7 +52,9 @@ Changing execution order is simple:
|
||||
📄 0.my-module.ts
|
||||
📄 1.another-module.ts
|
||||
```
|
||||
|
||||
- If you need to run an NPM module after user modules, just make it a user module and prefix the filename with a number!
|
||||
|
||||
```ts
|
||||
// modules/2.i18n.ts
|
||||
export { default } from '@wxt-dev/i18n/module';
|
||||
|
||||
@@ -128,7 +128,7 @@ If a version is not present in your `package.json`, it defaults to `"0.0.0"`.
|
||||
|
||||
WXT automatically discovers your extension's icon by looking at files in the `public/` directory:
|
||||
|
||||
```
|
||||
```plaintext
|
||||
public/
|
||||
├─ icon-16.png
|
||||
├─ icon-24.png
|
||||
@@ -159,6 +159,30 @@ export default defineConfig({
|
||||
|
||||
Alternatively, you can use [`@wxt-dev/auto-icons`](https://www.npmjs.com/package/@wxt-dev/auto-icons) to let WXT generate your icon at the required sizes.
|
||||
|
||||
### Firefox `theme_icons`
|
||||
|
||||
Firefox supports a [`theme_icons`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action#theme_icons) field on the toolbar action that swaps between a light and dark variant based on the current browser theme.
|
||||
|
||||
When [targeting Firefox](/guide/essentials/target-different-browsers.md), WXT auto-discovers paired light/dark icons in the `public/` directory and attaches them to `action` (MV3) or `browser_action` (MV2). You only need to drop both variants next to your regular icons:
|
||||
|
||||
```plaintext
|
||||
public/
|
||||
├─ icon-16.png # regular (default_icon)
|
||||
├─ icon-light-16.png # Firefox light theme
|
||||
├─ icon-dark-16.png # Firefox dark theme
|
||||
├─ icon-32.png
|
||||
├─ icon-light-32.png
|
||||
└─ icon-dark-32.png
|
||||
```
|
||||
|
||||
A size is only included in `theme_icons` if **both** a light and a dark file are present. The following filename patterns are discovered:
|
||||
|
||||
<<< @/../packages/wxt/src/core/utils/theme-icons.ts#snippet
|
||||
|
||||
Only `.png` files are discovered today, even though Firefox supports `.svg` - follow [#1120](https://github.com/wxt-dev/wxt/issues/1120) for updates.
|
||||
|
||||
If you set `manifest.action.theme_icons` (or `manifest.browser_action.theme_icons`) explicitly in `wxt.config.ts`, WXT will not overwrite it.
|
||||
|
||||
## Permissions
|
||||
|
||||
> [Chrome docs](https://developer.chrome.com/docs/extensions/reference/permissions/)
|
||||
@@ -176,6 +200,23 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
:::warning
|
||||
|
||||
Different browsers support different permissions. You are responsible for passing only the permissions required for each browser:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: ({ browser }) => ({
|
||||
permissions:
|
||||
browser === 'chrome'
|
||||
? ['storage', 'favicon', 'declarativeNetRequest']
|
||||
: ['storage', 'webRequest'],
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Host Permissions
|
||||
|
||||
> [Chrome docs](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions#host-permissions)
|
||||
@@ -235,6 +276,7 @@ If you want to use the `activeTab` permission or the `browser.action.onClicked`
|
||||
|
||||
1. Delete the [Popup entrypoint](/guide/essentials/entrypoints#popup) if it exists
|
||||
2. Add the `action` key to your manifest:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
|
||||
@@ -20,15 +20,15 @@ export default defineAppConfig({
|
||||
```
|
||||
|
||||
:::warning
|
||||
This file is committed to the repo, so don't put any secrets here. Instead, use [Environment Variables](#environment-variables)
|
||||
This file is committed to the repo, so don't put any secrets here. Instead, use [Environment Variables](/guide/essentials/config/environment-variables)
|
||||
:::
|
||||
|
||||
To access runtime config, WXT provides the `useAppConfig` function:
|
||||
To access runtime config, WXT provides the `getAppConfig` function:
|
||||
|
||||
```ts
|
||||
import { useAppConfig } from '#imports';
|
||||
import { getAppConfig } from '#imports';
|
||||
|
||||
console.log(useAppConfig()); // { theme: "dark" }
|
||||
console.log(getAppConfig()); // { theme: "dark" }
|
||||
```
|
||||
|
||||
## Environment Variables in App Config
|
||||
|
||||
@@ -17,28 +17,14 @@ Or if you're in a monorepo, you may not want to extend the config. If you don't
|
||||
/// <reference path="./.wxt/wxt.d.ts" />
|
||||
```
|
||||
|
||||
## Compiler Options
|
||||
|
||||
To specify custom compiler options, add them in `<rootDir>/tsconfig.json`:
|
||||
|
||||
```jsonc
|
||||
// <rootDir>/tsconfig.json
|
||||
{
|
||||
"extends": ".wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## TSConfig Paths
|
||||
|
||||
WXT provides a default set of path aliases.
|
||||
|
||||
| Alias | To | Example |
|
||||
| ----- | ------------- | ----------------------------------------------- |
|
||||
| `~~` | `<rootDir>/*` | `import "~~/scripts"` |
|
||||
| `@@` | `<rootDir>/*` | `import "@@/scripts"` |
|
||||
| `~~` | `<rootDir>/*` | `import "~~/package.json"` |
|
||||
| `@@` | `<rootDir>/*` | `import "@@/package.json"` |
|
||||
| `~` | `<srcDir>/*` | `import { toLowerCase } from "~/utils/strings"` |
|
||||
| `@` | `<srcDir>/*` | `import { toLowerCase } from "@/utils/strings"` |
|
||||
|
||||
@@ -63,3 +49,34 @@ export default defineConfig({
|
||||
import { fakeTab } from 'testing/fake-objects';
|
||||
import { toLowerCase } from 'strings';
|
||||
```
|
||||
|
||||
## Custom Options
|
||||
|
||||
To specify custom compiler or top-level options, you have two options:
|
||||
|
||||
1. Override WXT's value completely by setting it in your root `tsconfig.json`:
|
||||
|
||||
```jsonc
|
||||
// <rootDir>/tsconfig.json
|
||||
{
|
||||
"extends": ".wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
2. Merge custom values into values generated by WXT via the `prepare:tsconfig` hook:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
hooks: {
|
||||
'prepare:tsconfig': (wxt, { tsconfig }) => {
|
||||
tsconfig.compilerOptions.lib.push('WebWorker');
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Overriding is great for simple boolean values, but not for complex options like `paths` or `libs`, where you can't just "add" a value to the object from your `tsconfig.json` file. So WXT provides the hook that let's you add or merge or delete or do whatever you want to the config file before it's written to the `.wxt/` directory.
|
||||
|
||||
@@ -19,7 +19,7 @@ export default defineContentScript({
|
||||
|
||||
This object is responsible for tracking whether or not the content script's context is "invalidated". Most browsers, by default, do not stop content scripts if the extension is uninstalled, updated, or disabled. When this happens, content scripts start reporting this error:
|
||||
|
||||
```
|
||||
```plaintext
|
||||
Error: Extension context invalidated.
|
||||
```
|
||||
|
||||
@@ -76,6 +76,7 @@ To create a standalone content script that only includes a CSS file:
|
||||
|
||||
1. Create the CSS file: `entrypoints/example.content.css`
|
||||
2. Use the `build:manifestGenerated` hook to add the content script to the manifest:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
hooks: {
|
||||
@@ -210,9 +211,7 @@ export default defineContentScript({
|
||||
anchor: 'body',
|
||||
onMount: (container) => {
|
||||
// Create the Svelte app inside the UI container
|
||||
mount(App, {
|
||||
target: container,
|
||||
});
|
||||
return mount(App, { target: container });
|
||||
},
|
||||
onRemove: (app) => {
|
||||
// Destroy the app when the UI is removed
|
||||
@@ -395,11 +394,9 @@ export default defineContentScript({
|
||||
anchor: 'body',
|
||||
onMount: (container) => {
|
||||
// Create the Svelte app inside the UI container
|
||||
mount(App, {
|
||||
target: container,
|
||||
});
|
||||
return mount(App, { target: container });
|
||||
},
|
||||
onRemove: () => {
|
||||
onRemove: (app) => {
|
||||
// Destroy the app when the UI is removed
|
||||
unmount(app);
|
||||
},
|
||||
@@ -452,6 +449,15 @@ Full examples:
|
||||
- [react-content-script-ui](https://github.com/wxt-dev/examples/tree/main/examples/react-content-script-ui)
|
||||
- [tailwindcss](https://github.com/wxt-dev/examples/tree/main/examples/tailwindcss)
|
||||
|
||||
:::warning `rem` Units Are Not Fully Isolated
|
||||
|
||||
WXT resets most inherited styles via `all: initial`. This doesn't reset the `<html>` element's font size, which determines the relative size of `rem` units.
|
||||
|
||||
If your CSS framework uses `rem` units, like Tailwind CSS, you may notice your UI's scale changing on different websites.
|
||||
|
||||
See the [FAQ](/guide/resources/faq#my-content-script-ui-looks-different-on-certain-websites) for a fix.
|
||||
:::
|
||||
|
||||
### IFrame
|
||||
|
||||
If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**.
|
||||
@@ -459,6 +465,7 @@ If you don't need to run your UI in the same frame as the content script, you ca
|
||||
WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/utils/content-script-ui/iframe/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>
|
||||
@@ -473,7 +480,9 @@ WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/utils/cont
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
1. Add the page to the manifest's `web_accessible_resources`:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
@@ -486,6 +495,7 @@ WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/utils/cont
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
1. Create and mount the IFrame:
|
||||
|
||||
```ts
|
||||
@@ -590,6 +600,76 @@ For MV3, `injectScript` is synchronous and the injected script will be evaluated
|
||||
However for MV2, `injectScript` has to `fetch` the script's text content and create an inline `<script>` block. This means for MV2, your script is injected asynchronously and it will not be evaluated at the same time as your content script's `run_at`.
|
||||
:::
|
||||
|
||||
The `script` element can be modified just before it is added to the DOM by using the `modifyScript` option. This can be used to e.g. modify `script.async`/`script.defer`, add event listeners to the element, or pass data to the script via `script.dataset`. An example:
|
||||
|
||||
```ts
|
||||
// entrypoints/example.content.ts
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main() {
|
||||
await injectScript('/example-main-world.js', {
|
||||
modifyScript(script) {
|
||||
script.dataset['greeting'] = 'Hello there';
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/example-main-world.ts
|
||||
export default defineUnlistedScript(() => {
|
||||
console.log(document.currentScript?.dataset['greeting']);
|
||||
});
|
||||
```
|
||||
|
||||
`injectScript` returns the created script element. It can be used to e.g. send messages to the script in the form of custom events. The script can add an event listener for them via `document.currentScript`. An example of bidirectional communication:
|
||||
|
||||
```ts
|
||||
// entrypoints/example.content.ts
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main() {
|
||||
const { script } = await injectScript('/example-main-world.js', {
|
||||
modifyScript(script) {
|
||||
// Add a listener before the injected script is loaded.
|
||||
script.addEventListener('from-injected-script', (event) => {
|
||||
if (event instanceof CustomEvent) {
|
||||
console.log(`${event.type}:`, event.detail);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Send an event after the injected script is loaded.
|
||||
script.dispatchEvent(
|
||||
new CustomEvent('from-content-script', {
|
||||
detail: 'General Kenobi',
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/example-main-world.ts
|
||||
export default defineUnlistedScript(() => {
|
||||
const script = document.currentScript;
|
||||
|
||||
script?.addEventListener('from-content-script', (event) => {
|
||||
if (event instanceof CustomEvent) {
|
||||
console.log(`${event.type}:`, event.detail);
|
||||
}
|
||||
});
|
||||
|
||||
script?.dispatchEvent(
|
||||
new CustomEvent('from-injected-script', {
|
||||
detail: 'Hello there',
|
||||
}),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## Mounting UI to dynamic element
|
||||
|
||||
In many cases, you may need to mount a UI to a DOM element that does not exist at the time the web page is initially loaded. To handle this, use the `autoMount` API to automatically mount the UI when the target element appears dynamically and unmount it when the element disappears. In WXT, the `anchor` option is used to target the element, enabling automatic mounting and unmounting based on its appearance and removal.
|
||||
|
||||
@@ -4,50 +4,30 @@ outline: deep
|
||||
|
||||
# Entrypoints
|
||||
|
||||
WXT uses the files inside the `entrypoints/` directory as inputs when bundling your extension. They can be HTML, JS, CSS, or any variant of those file types supported by Vite (Pug, TS, JSX, SCSS, etc).
|
||||
WXT uses the files inside the `entrypoints/` directory as inputs when bundling your extension. They can be HTML, JS, CSS, or any variant of those file types supported by Vite (TS, JSX, SCSS, etc).
|
||||
|
||||
Here's an example set of entrypoints:
|
||||
## Folder Structure
|
||||
|
||||
Inside the `entrypoints/` directory, an entrypoint is defined as a single file or directory (with an `index` file) inside it.
|
||||
|
||||
:::code-group
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
```html [Single File]
|
||||
📂 entrypoints/
|
||||
📂 popup/
|
||||
📄 index.html
|
||||
📄 main.ts
|
||||
📄 style.css
|
||||
📄 background.ts
|
||||
📄 content.ts
|
||||
📄 {name}.{ext}
|
||||
```
|
||||
|
||||
[[toc]]
|
||||
<!-- prettier-ignore -->
|
||||
```html [Directory]
|
||||
📂 entrypoints/
|
||||
📂 {name}/
|
||||
📄 index.{ext}
|
||||
```
|
||||
|
||||
## Listed vs Unlisted
|
||||
|
||||
For web extensions, there are two types of entrypoints:
|
||||
|
||||
- **Listed**: Referenced in the `manifest.json`
|
||||
- **Unlisted**: Not referenced in the `manifest.json`
|
||||
|
||||
Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:
|
||||
|
||||
- Popup
|
||||
- Options
|
||||
- Background
|
||||
- Content Scripts
|
||||
- Etc.
|
||||
|
||||
Some examples of "unlisted" entrypoints:
|
||||
|
||||
- A welcome page shown when the extension is installed
|
||||
- JS files injected by content scripts into the page's main world
|
||||
|
||||
:::tip
|
||||
Regardless of whether an entrypoint is listed or unlisted, it will still be bundled into your extension and be available at runtime.
|
||||
:::
|
||||
|
||||
## Adding Entrypoints
|
||||
|
||||
An entrypoint can be defined as a single file or directory with an `index` file inside it.
|
||||
The entrypoint's `name` dictates the type of entrypoint. For example, to add a ["Background" entrypoint](#background), either of these files would work:
|
||||
|
||||
:::code-group
|
||||
|
||||
@@ -66,10 +46,96 @@ An entrypoint can be defined as a single file or directory with an `index` file
|
||||
|
||||
:::
|
||||
|
||||
The entrypoint's name dictates the type of entrypoint, listed vs unlisted. In this example, "background" is the name of the ["Background" entrypoint](#background).
|
||||
|
||||
Refer to the [Entrypoint Types](#entrypoint-types) section for the full list of listed entrypoints and their filename patterns.
|
||||
|
||||
### Including Other Files
|
||||
|
||||
When using an entrypoint directory, `entrypoints/{name}/index.{ext}`, you can add related files next to the `index` file.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 entrypoints/
|
||||
📂 popup/
|
||||
📄 index.html ← This file is the entrypoint
|
||||
📄 main.ts
|
||||
📄 style.css
|
||||
📂 background/
|
||||
📄 index.ts ← This file is the entrypoint
|
||||
📄 alarms.ts
|
||||
📄 messaging.ts
|
||||
📂 youtube.content/
|
||||
📄 index.ts ← This file is the entrypoint
|
||||
📄 style.css
|
||||
```
|
||||
|
||||
:::danger
|
||||
**DO NOT** put files related to an entrypoint directly inside the `entrypoints/` directory. WXT will treat them as entrypoints and try to build them, usually resulting in an error.
|
||||
|
||||
Instead, use a directory for that entrypoint:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 entrypoints/
|
||||
📄 popup.html <!-- [!code --] -->
|
||||
📄 popup.ts <!-- [!code --] -->
|
||||
📄 popup.css <!-- [!code --] -->
|
||||
📂 popup/ <!-- [!code ++] -->
|
||||
📄 index.html <!-- [!code ++] -->
|
||||
📄 main.ts <!-- [!code ++] -->
|
||||
📄 style.css <!-- [!code ++] -->
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Deeply Nested Entrypoints
|
||||
|
||||
While the `entrypoints/` directory might resemble the `pages/` directory of other web frameworks, like Nuxt or Next.js, **it does not support deeply nesting entrypoints** in the same way.
|
||||
|
||||
Entrypoints must be zero or one levels deep for WXT to discover and build them:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 entrypoints/
|
||||
📂 youtube/ <!-- [!code --] -->
|
||||
📂 content/ <!-- [!code --] -->
|
||||
📄 index.ts <!-- [!code --] -->
|
||||
📄 ... <!-- [!code --] -->
|
||||
📂 injected/ <!-- [!code --] -->
|
||||
📄 index.ts <!-- [!code --] -->
|
||||
📄 ... <!-- [!code --] -->
|
||||
📂 youtube.content/ <!-- [!code ++] -->
|
||||
📄 index.ts <!-- [!code ++] -->
|
||||
📄 ... <!-- [!code ++] -->
|
||||
📂 youtube-injected/ <!-- [!code ++] -->
|
||||
📄 index.ts <!-- [!code ++] -->
|
||||
📄 ... <!-- [!code ++] -->
|
||||
```
|
||||
|
||||
## Unlisted Entrypoints
|
||||
|
||||
In web extensions, there are two types of entrypoints:
|
||||
|
||||
1. **Listed**: Referenced in the `manifest.json`
|
||||
2. **Unlisted**: Not referenced in the `manifest.json`
|
||||
|
||||
Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:
|
||||
|
||||
- Popup
|
||||
- Options
|
||||
- Background
|
||||
- Content Script
|
||||
|
||||
However, not all entrypoints in web extensions are listed in the manifest. Some are not listed in the manifest, but are still used by extensions. For example:
|
||||
|
||||
- A welcome page shown in a new tab when the extension is installed
|
||||
- JS files injected by content scripts into the main world
|
||||
|
||||
For more details on how to add unlisted entrypoints, see:
|
||||
|
||||
- [Unlisted Pages](#unlisted-pages)
|
||||
- [Unlisted Scripts](#unlisted-scripts)
|
||||
- [Unlisted CSS](#unlisted-css)
|
||||
|
||||
## Defining Manifest Options
|
||||
|
||||
Most listed entrypoints have options that need to be added to the `manifest.json`. However with WXT, instead of defining the options in a separate file, _you define these options inside the entrypoint file itself_.
|
||||
@@ -106,8 +172,6 @@ When building your extension, WXT will look at the options defined in your entry
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/background/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background)
|
||||
|
||||
For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['background.[jt]s', 'background.js'],
|
||||
@@ -141,6 +205,25 @@ export default defineBackground({
|
||||
|
||||
:::
|
||||
|
||||
For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.
|
||||
|
||||
When defining your background entrypoint, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
browser.action.onClicked.addListener(() => { // [!code --]
|
||||
// ... // [!code --]
|
||||
}); // [!code --]
|
||||
|
||||
export default defineBackground(() => {
|
||||
browser.action.onClicked.addListener(() => { // [!code ++]
|
||||
// ... // [!code ++]
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.
|
||||
|
||||
### Bookmarks
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
@@ -169,18 +252,18 @@ export default defineBackground({
|
||||
</html>
|
||||
```
|
||||
|
||||
When you define a Bookmarks entrypoint, WXT will automatically update the manifest to override the browser's bookmarks page with your own HTML page.
|
||||
|
||||
### Content Scripts
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/content_scripts/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts)
|
||||
|
||||
See [Content Script UI](/guide/essentials/content-scripts) for more info on creating UIs and including CSS in content scripts.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['content.[jt]sx?', 'content-scripts/content.js'],
|
||||
['content/index.[jt]sx?', 'content-scripts/content.js'],
|
||||
['<name>.content.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
['<name>.content/index.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
['{name}.content.[jt]sx?', 'content-scripts/{name}.js'],
|
||||
['{name}.content/index.[jt]sx?', 'content-scripts/{name}.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -213,12 +296,29 @@ export default defineContentScript({
|
||||
});
|
||||
```
|
||||
|
||||
When defining content script entrypoints, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
const container = document.createElement('div'); // [!code --]
|
||||
document.body.append(container); // [!code --]
|
||||
|
||||
export default defineContentScript({
|
||||
main: function () {
|
||||
const container = document.createElement('div'); // [!code ++]
|
||||
document.body.append(container); // [!code ++]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.
|
||||
|
||||
See [Content Script UI](/guide/essentials/content-scripts) for more info on creating UIs and including CSS in content scripts.
|
||||
|
||||
### Devtools
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/devtools/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/devtools_page)
|
||||
|
||||
Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/examples/devtools-extension#readme) to add different panels and panes.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['devtools.html', 'devtools.html'],
|
||||
@@ -242,6 +342,8 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
</html>
|
||||
```
|
||||
|
||||
Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/examples/devtools-extension#readme) to add different panels and panes.
|
||||
|
||||
### History
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
@@ -270,6 +372,8 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
</html>
|
||||
```
|
||||
|
||||
When you define a History entrypoint, WXT will automatically update the manifest to override the browser's history page with your own HTML page.
|
||||
|
||||
### Newtab
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
@@ -298,6 +402,8 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
</html>
|
||||
```
|
||||
|
||||
When you define a Newtab entrypoint, WXT will automatically update the manifest to override the browser's new tab page with your own HTML page.
|
||||
|
||||
### Options
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/options/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui)
|
||||
@@ -357,13 +463,26 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
16: '/icon-16.png',
|
||||
24: '/icon-24.png',
|
||||
'16': '/icon-16.png',
|
||||
'24': '/icon-24.png',
|
||||
...
|
||||
}"
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
<!-- Firefox only: where to place the action button -->
|
||||
<meta
|
||||
name="manifest.default_area"
|
||||
content="navbar|menupanel|tabstrip|personaltoolbar"
|
||||
/>
|
||||
<!-- Firefox only: icons for light/dark themes -->
|
||||
<meta
|
||||
name="manifest.theme_icons"
|
||||
content="[
|
||||
{ light: '/icon-light-16.png', dark: '/icon-dark-16.png', size: 16 },
|
||||
{ light: '/icon-light-32.png', dark: '/icon-dark-32.png', size: 32 }
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
@@ -387,8 +506,8 @@ Firefox does not support sandboxed pages.
|
||||
:patterns="[
|
||||
['sandbox.html', 'sandbox.html'],
|
||||
['sandbox/index.html', 'sandbox.html'],
|
||||
['<name>.sandbox.html', '<name>.html'],
|
||||
['<name>.sandbox/index.html', '<name>.html'],
|
||||
['{name}.sandbox.html', '{name}.html'],
|
||||
['{name}.sandbox/index.html', '{name}.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -414,14 +533,12 @@ Firefox does not support sandboxed pages.
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)
|
||||
|
||||
In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar_action` API.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['sidepanel.html', 'sidepanel.html'],
|
||||
['sidepanel/index.html', 'sidepanel.html'],
|
||||
['<name>.sidepanel.html', '<name>.html` '],
|
||||
['<name>.sidepanel/index.html', '<name>.html` '],
|
||||
['{name}.sidepanel.html', '{name}.html` '],
|
||||
['{name}.sidepanel/index.html', '{name}.html` '],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -437,8 +554,8 @@ In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
16: '/icon-16.png',
|
||||
24: '/icon-24.png',
|
||||
'16': '/icon-16.png',
|
||||
'24': '/icon-24.png',
|
||||
...
|
||||
}"
|
||||
/>
|
||||
@@ -455,20 +572,18 @@ In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar
|
||||
</html>
|
||||
```
|
||||
|
||||
In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar_action` API.
|
||||
|
||||
### Unlisted CSS
|
||||
|
||||
Follow Vite's guide to setup your preprocessor of choice: https://vitejs.dev/guide/features.html#css-pre-processors
|
||||
|
||||
CSS entrypoints are always unlisted. To add CSS to a content script, see the [Content Script](/guide/essentials/content-scripts#css) docs.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.(css|scss|sass|less|styl|stylus)', '<name>.css'],
|
||||
['<name>/index.(css|scss|sass|less|styl|stylus)', '<name>.css'],
|
||||
['{name}.(css|scss|sass|less|styl|stylus)', '{name}.css'],
|
||||
['{name}/index.(css|scss|sass|less|styl|stylus)', '{name}.css'],
|
||||
['content.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],
|
||||
['content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],
|
||||
['<name>.content.(css|scss|sass|less|styl|stylus)', 'content-scripts/<name>.css'],
|
||||
['<name>.content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/<name>.css'],
|
||||
['{name}.content.(css|scss|sass|less|styl|stylus)', 'content-scripts/{name}.css'],
|
||||
['{name}.content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/{name}.css'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -478,12 +593,16 @@ body {
|
||||
}
|
||||
```
|
||||
|
||||
Follow Vite's guide to setup your preprocessor of choice: <https://vitejs.dev/guide/features.html#css-pre-processors>
|
||||
|
||||
CSS entrypoints are always unlisted. To add CSS to a content script, see the [Content Script](/guide/essentials/content-scripts#css) docs.
|
||||
|
||||
### Unlisted Pages
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.html', '<name>.html'],
|
||||
['<name>/index.html', '<name>.html'],
|
||||
['{name}.html', '{name}.html'],
|
||||
['{name}/index.html', '{name}.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -505,20 +624,21 @@ body {
|
||||
</html>
|
||||
```
|
||||
|
||||
Pages are accessible at `/<name>.html`:
|
||||
At runtime, unlisted pages are accessible at `/{name}.html`:
|
||||
|
||||
```ts
|
||||
const url = browser.runtime.getURL('/<name>.html');
|
||||
const url = browser.runtime.getURL('/{name}.html');
|
||||
|
||||
console.log(url); // "chrome-extension://<id>/<name>.html"
|
||||
console.log(url); // "chrome-extension://{id}/{name}.html"
|
||||
window.open(url); // Open the page in a new tab
|
||||
```
|
||||
|
||||
### Unlisted Scripts
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.[jt]sx?', '<name>.js'],
|
||||
['<name>/index.[jt]sx?', '<name>.js'],
|
||||
['{name}.[jt]sx?', '{name}.js'],
|
||||
['{name}/index.[jt]sx?', '{name}.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -544,12 +664,29 @@ export default defineUnlistedScript({
|
||||
|
||||
:::
|
||||
|
||||
Scripts are accessible from `/<name>.js`:
|
||||
At runtime, unlisted scripts are accessible from `/{name}.js`:
|
||||
|
||||
```ts
|
||||
const url = browser.runtime.getURL('/<name>.js');
|
||||
const url = browser.runtime.getURL('/{name}.js');
|
||||
|
||||
console.log(url); // "chrome-extension://<id>/<name>.js"
|
||||
console.log(url); // "chrome-extension://{id}/{name}.js"
|
||||
```
|
||||
|
||||
You are responsible for loading/running these scripts where needed. If necessary, don't forget to add the script and/or any related assets to [`web_accessible_resources`](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).
|
||||
|
||||
When defining an unlisted script, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
document.querySelectorAll('a').forEach((anchor) => { // [!code --]
|
||||
// ... // [!code --]
|
||||
}); // [!code --]
|
||||
|
||||
export default defineUnlistedScript(() => {
|
||||
document.querySelectorAll('a').forEach((anchor) => { // [!code ++]
|
||||
// ... // [!code ++]
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ES Modules
|
||||
|
||||
You source code should always be written as ESM. However, you have some control whether an entrypoint is bundled as ESM.
|
||||
Your source code should always be written as ESM. However, you have some control whether an entrypoint is bundled as ESM.
|
||||
|
||||
## HTML Pages <Badge type="warning" text="≥0.0.1" />
|
||||
|
||||
@@ -25,7 +25,7 @@ export default defineBackground({
|
||||
});
|
||||
```
|
||||
|
||||
This will change the output format to ESM, enable code-spliting between your background script and HTML pages, and set `"type": "module"` in your manifest.
|
||||
This will change the output format to ESM, enable code-splitting between your background script and HTML pages, and set `"type": "module"` in your manifest.
|
||||
|
||||
:::warning
|
||||
Only MV3 supports ESM background scripts/service workers. When targeting MV2, the `type` option is ignored and the background is always bundled into a single file as IIFE.
|
||||
|
||||
@@ -29,7 +29,7 @@ This means you can use the promise-style API for both MV2 and MV3, and it will w
|
||||
All types can be accessed via WXT's `Browser` namespace:
|
||||
|
||||
```ts
|
||||
import { Browser } from 'wxt/browser';
|
||||
import { type Browser } from 'wxt/browser';
|
||||
|
||||
function handleMessage(message: any, sender: Browser.runtime.MessageSender) {
|
||||
// ...
|
||||
@@ -75,3 +75,85 @@ Alternatively, if you're trying to use similar APIs under different names (to su
|
||||
//
|
||||
});
|
||||
```
|
||||
|
||||
### Augmenting the Browser Type
|
||||
|
||||
WXT's `browser` types are based on the `@types/chrome` package. That means some Firefox-specific APIs may not be typed, like `browser.sidebarAction`. If you want to add types for these APIs, you can augment the browser type to add them yourself:
|
||||
|
||||
```ts
|
||||
// <srcDir>/browser-types.d.ts
|
||||
import '@wxt-dev/browser';
|
||||
import type { SidebarAction } from 'webextension-polyfill';
|
||||
|
||||
declare module '@wxt-dev/browser' {
|
||||
namespace Browser {
|
||||
export const sidebarAction: SidebarAction.Static;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> For this to work, you may need to install `@wxt-dev/browser` as a direct dependency.
|
||||
>
|
||||
> ```sh
|
||||
> pnpm add @wxt-dev/browser
|
||||
> ```
|
||||
|
||||
## Entrypoint Limitations
|
||||
|
||||
Because WXT imports your entrypoint files into a NodeJS, non-extension environment, the `chrome`/`browser` variables provided to extensions by the browser **will not be available**.
|
||||
|
||||
To prevent some basic errors, WXT polyfills these globals with the same in-memory, fake implementation it uses for testing: [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation/). However, not all the APIs have been implemented.
|
||||
|
||||
So it is extremely important to NEVER use `browser.*` extension APIs outside the main function of any JS/TS entrypoints (background, content scripts, and unlisted scripts). If you do, you'll see an error like this:
|
||||
|
||||
```plaintext
|
||||
✖ Command failed after 440 ms
|
||||
|
||||
ERROR Browser.action.onClicked.addListener not implemented.
|
||||
```
|
||||
|
||||
The fix is simple, just move your API usage into the entrypoint's main function:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [background.ts]
|
||||
browser.action.onClicked.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code --]
|
||||
|
||||
export default defineBackground(() => {
|
||||
browser.action.onClicked.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
```ts [content.ts]
|
||||
browser.runtime.onMessage.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code --]
|
||||
|
||||
export default defineContentScript({
|
||||
main() {
|
||||
browser.runtime.onMessage.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code ++]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [unlisted.ts]
|
||||
browser.runtime.onMessage.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code --]
|
||||
|
||||
export default defineUnlistedScript(() => {
|
||||
browser.runtime.onMessage.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Read [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) for more technical details about this limitation.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Favicons
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/how-to/ui/favicons)
|
||||
|
||||
Chromium-based browsers expose cached favicons through the `_favicon/` URL served from your extension. To use it, declare the `favicon` permission in your manifest and call `browser.runtime.getURL` with a `_favicon/` path:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['favicon'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// Any entrypoint
|
||||
function getFaviconUrl(pageUrl: string, size = 16) {
|
||||
const url = new URL(browser.runtime.getURL('/_favicon/'));
|
||||
url.searchParams.set('pageUrl', pageUrl);
|
||||
url.searchParams.set('size', String(size));
|
||||
return url.toString();
|
||||
}
|
||||
```
|
||||
|
||||
::: warning Chromium only
|
||||
The favicon API is only available on Chromium-based browsers. Firefox has no equivalent, so `/_favicon/` URLs will not resolve at runtime there. If your extension supports both browsers, gate favicon usage behind `import.meta.env.CHROME` (or similar) and declare the permission per-browser.
|
||||
:::
|
||||
|
||||
## Usage from a content script
|
||||
|
||||
WXT does **not** add a `web_accessible_resources` entry for `_favicon/*` — not every extension needs one, and adding it unconditionally would expose internals to sites that don't need them. If you want to load a favicon inside a content script (for example, as an `<img src>`), add your own entry in `wxt.config.ts`:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['favicon'],
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ['_favicon/*'],
|
||||
matches: ['<all_urls>'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -97,7 +97,7 @@ Instead, you need to configure the router to run in "hash" mode, where the routi
|
||||
|
||||
Refer to your router's docs for information about hash mode and how to enable it. Here's a non-extensive list of a few popular routers:
|
||||
|
||||
- [`react-router`](https://reactrouter.com/en/main/routers/create-hash-router)
|
||||
- [`react-router`](https://reactrouter.com/api/data-routers/createHashRouter#createhashrouter)
|
||||
- [`vue-router`](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode)
|
||||
- [`svelte-spa-router`](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing)
|
||||
- [`solid-router`](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router)
|
||||
|
||||
@@ -9,6 +9,7 @@ This page discusses how to setup internationalization using the vanilla `browser
|
||||
## Usage
|
||||
|
||||
1. Add `default_locale` to your manifest:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
@@ -16,11 +17,12 @@ This page discusses how to setup internationalization using the vanilla `browser
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
2. Create `messages.json` files in the `public/` directory:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 {srcDir}/
|
||||
📂 {rootDir}/
|
||||
📂 public/
|
||||
📂 _locales/
|
||||
📂 en/
|
||||
@@ -41,9 +43,11 @@ This page discusses how to setup internationalization using the vanilla `browser
|
||||
```
|
||||
|
||||
3. Get the translation:
|
||||
|
||||
```ts
|
||||
browser.i18n.getMessage('helloWorld');
|
||||
```
|
||||
|
||||
4. _Optional_: Add translations for extension name and description:
|
||||
|
||||
```json
|
||||
|
||||
@@ -30,9 +30,10 @@ Here's a brief summary of each of these files and directories:
|
||||
- `.wxt/`: Generated by WXT, it contains TS config
|
||||
- `assets/`: Contains all CSS, images, and other assets that should be processed by WXT
|
||||
- `components/`: Auto-imported by default, contains UI components
|
||||
- `composables/`: Auto-imported by default, contains composable functions for Vue
|
||||
- `composables/`: Auto-imported by default, contains source code for your project's composable functions for Vue
|
||||
- `entrypoints/`: Contains all the entrypoints that get bundled into your extension
|
||||
- `hooks/`: Auto-imported by default, contains hooks for React and Solid
|
||||
- `hooks/`: Auto-imported by default, contains source code for your project's hooks for React and Solid
|
||||
- `modules/`: Contains [local WXT Modules](/guide/essentials/wxt-modules) for your project
|
||||
- `public/`: Contains any files you want to copy into the output folder as-is, without being processed by WXT
|
||||
- `utils/`: Auto-imported by default, contains generic utilities used throughout your project
|
||||
- `.env`: Contains [Environment Variables](/guide/essentials/config/environment-variables)
|
||||
|
||||
@@ -23,7 +23,7 @@ WXT provides two commands to help automate submitting a new version for review a
|
||||
- `wxt submit init`: Setup all the required secrets and options for the `wxt submit` command
|
||||
- `wxt submit`: Submit new versions of your extension for review (and publish them automatically once approved)
|
||||
|
||||
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.
|
||||
To get started, run `wxt submit init` and follow the prompts, or run `wxt submit --help` to view all available options. 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.
|
||||
|
||||
@@ -72,13 +72,13 @@ jobs:
|
||||
submit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -163,8 +163,8 @@ yarn zip:firefox
|
||||
```
|
||||
|
||||
```sh [bun]
|
||||
bun i
|
||||
bun zip:firefox
|
||||
bun install
|
||||
bun run zip:firefox
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -206,13 +206,13 @@ WXT uses the command `npm pack <package-name>` to download the package. That mea
|
||||
|
||||
WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT does not create yet. For now, if you want to publish to Safari, follow this guide:
|
||||
|
||||
- [Converting a web extension for Safari](https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari) - "Convert your existing extension to a Safari web extension using Xcode’s command-line tool."
|
||||
- [Packaging a web extension for Safari](https://developer.apple.com/documentation/safariservices/packaging-a-web-extension-for-safari) - "Package your existing extension as a Safari web extension using Xcode’s command-line tool."
|
||||
|
||||
When running the `safari-web-extension-converter` CLI tool, pass the `.output/safari-mv2` or `.output/safari-mv3` directory, not your source code directory.
|
||||
When running the `safari-web-extension-packager` 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
|
||||
xcrun safari-web-extension-packager .output/safari-mv2
|
||||
```
|
||||
|
||||
### Edge Addons
|
||||
|
||||
@@ -62,6 +62,7 @@ Here are some examples:
|
||||
```
|
||||
|
||||
- HTML file only built for all targets other than `chrome`:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -77,3 +78,28 @@ Here are some examples:
|
||||
```
|
||||
|
||||
Alternatively, you can use the [`filterEntrypoints` config](/api/reference/wxt/interfaces/InlineConfig#filterentrypoints) to list all the entrypoints you want to build.
|
||||
|
||||
## Per-Browser Options
|
||||
|
||||
Some entrypoint options can be customized per build target by passing an object keyed by the [target browser](/guide/essentials/target-different-browsers#target-a-browser) instead of a single value. This is useful when different browsers need different match patterns, run timings, or other entrypoint behavior:
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
matches: {
|
||||
chrome: ['*://chrome.example.com/*'],
|
||||
firefox: ['*://firefox.example.com/*'],
|
||||
},
|
||||
runAt: {
|
||||
chrome: 'document_start',
|
||||
firefox: 'document_end',
|
||||
},
|
||||
|
||||
world: {
|
||||
firefox: 'MAIN',
|
||||
},
|
||||
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ WXT provides first class support for Vitest for unit testing:
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { WxtVitest } from 'wxt/testing';
|
||||
import { WxtVitest } from 'wxt/testing/vitest-plugin';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [WxtVitest()],
|
||||
@@ -36,7 +36,7 @@ This example demonstrates that you don't have to mock `browser.storage` (used by
|
||||
|
||||
```ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { fakeBrowser } from 'wxt/testing';
|
||||
import { fakeBrowser } from 'wxt/testing/fake-browser';
|
||||
|
||||
const accountStorage = storage.defineItem<Account>('local:account');
|
||||
|
||||
@@ -85,7 +85,7 @@ import { injectScript, createShadowRootUi } from '#imports';
|
||||
But Vitest sees this:
|
||||
|
||||
```ts
|
||||
import { injectScript } from 'wxt/browser';
|
||||
import { injectScript } from 'wxt/utils/inject-script';
|
||||
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
|
||||
```
|
||||
|
||||
@@ -105,4 +105,4 @@ To use a different framework, you will likely have to disable auto-imports, setu
|
||||
|
||||
It is possible to do, but will require a bit more setup. Refer to Vitest's setup for an example of how to setup a test environment:
|
||||
|
||||
https://github.com/wxt-dev/wxt/blob/main/packages/wxt/src/testing/wxt-vitest-plugin.ts
|
||||
<https://github.com/wxt-dev/wxt/blob/main/packages/wxt/src/testing/wxt-vitest-plugin.ts>
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# WXT Modules
|
||||
|
||||
WXT provides a "module system" that let's you run code at different steps in the build process to modify it.
|
||||
WXT provides a "module system" that lets you run code at different steps in the build process to modify it.
|
||||
|
||||
[[toc]]
|
||||
|
||||
## Adding a Module
|
||||
## Installing a Module
|
||||
|
||||
There are two ways to add a module to your project:
|
||||
To use a published module from NPM, install the package and add it to your config:
|
||||
|
||||
1. **NPM**: install an NPM package, like [`@wxt-dev/auto-icons`](https://www.npmjs.com/package/@wxt-dev/auto-icons) and add it to your config:
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/auto-icons'],
|
||||
});
|
||||
```
|
||||
> Searching for ["wxt module"](https://www.npmjs.com/search?q=wxt%20module) on NPM is a good way to find published WXT modules.
|
||||
2. **Local**: add a file to your project's `modules/` directory:
|
||||
```
|
||||
<rootDir>/
|
||||
modules/
|
||||
my-module.ts
|
||||
```
|
||||
> To learn more about writing your own modules, read the [Writing Modules](/guide/essentials/wxt-modules) docs.
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/auto-icons'],
|
||||
});
|
||||
```
|
||||
|
||||
> Searching for ["wxt module"](https://www.npmjs.com/search?q=wxt%20module) on NPM is a good way to find published WXT modules.
|
||||
|
||||
## Module Options
|
||||
|
||||
@@ -40,7 +37,7 @@ Modules are loaded in the same order as hooks are executed. Refer to the [Hooks
|
||||
|
||||
## Writing Modules
|
||||
|
||||
Here's what a basic WXT module looks like:
|
||||
If you need custom build logic for your project, you can write your own local module. Here's what a basic WXT module looks like:
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
@@ -52,6 +49,14 @@ export default defineWxtModule({
|
||||
});
|
||||
```
|
||||
|
||||
To add it to your project, place the file in the `modules/` directory at the root of your project. Any module file in this directory is **automatically discovered and loaded** — no additional configuration is needed:
|
||||
|
||||
```plaintext
|
||||
<rootDir>/
|
||||
modules/
|
||||
my-module.ts ← loaded automatically
|
||||
```
|
||||
|
||||
Each module's setup function is executed after the `wxt.config.ts` file is loaded. The `wxt` object provides everything you need to write a module:
|
||||
|
||||
- Use `wxt.hook(...)` to hook into the build's lifecycle and make changes
|
||||
@@ -61,7 +66,7 @@ Each module's setup function is executed after the `wxt.config.ts` file is loade
|
||||
|
||||
Refer to the [API reference](/api/reference/wxt/interfaces/Wxt) for a complete list of properties and functions available.
|
||||
|
||||
Also to make sure and read about all the [hooks that are available](https://wxt.dev/api/reference/wxt/interfaces/WxtHooks) - they are essential to writing modules.
|
||||
Also make sure to read about [all the hooks that are available](/api/reference/wxt/interfaces/WxtHooks) - they are essential to writing modules.
|
||||
|
||||
### Recipes
|
||||
|
||||
@@ -126,12 +131,93 @@ declare module 'wxt/utils/define-app-config' {
|
||||
Runtime options are returned when calling
|
||||
|
||||
```ts
|
||||
const config = useAppConfig();
|
||||
const config = getAppConfig();
|
||||
console.log(config.myModule);
|
||||
```
|
||||
|
||||
This is very useful when [generating runtime code](#generate-runtime-module).
|
||||
|
||||
#### Logging
|
||||
|
||||
There are two "correct" ways to add console logs to your modules:
|
||||
|
||||
1. Use `wxt.logger` for info, warnings, and errors
|
||||
2. Install [`obug`](https://www.npmjs.com/package/obug) for debug messages
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
import { createDebug } from 'obug';
|
||||
|
||||
const debug = createDebug('my-module');
|
||||
|
||||
export default defineWxtModule({
|
||||
setup(wxt) {
|
||||
wxt.logger.info('Module loaded');
|
||||
debug('Debug details');
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`wxt.logger` is great for formatted, pretty messages that match the rest of WXT's logs.
|
||||
|
||||
`obug` makes it easy for devs to enable and filter debug logs when necessary:
|
||||
|
||||
```sh
|
||||
DEBUG=my-module wxt dev
|
||||
```
|
||||
|
||||
#### Add custom entrypoint options
|
||||
|
||||
Modules can add custom options to entrypoints by augmenting the entrypoint options types. This allows you to add custom configuration that can be accessed during the build process.
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
import 'wxt';
|
||||
|
||||
declare module 'wxt' {
|
||||
export interface BackgroundEntrypointOptions {
|
||||
// Add custom options to the background entrypoint
|
||||
myCustomOption?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineWxtModule({
|
||||
setup(wxt) {
|
||||
wxt.hook('entrypoints:resolved', (_, entrypoints) => {
|
||||
const background = entrypoints.find((e) => e.type === 'background');
|
||||
if (background) {
|
||||
console.log('Custom option:', background.options.myCustomOption);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Now users can set the custom option in their entrypoint:
|
||||
|
||||
```ts [entrypoints/background.ts]
|
||||
export default defineBackground({
|
||||
myCustomOption: 'custom value',
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This works for all other JS and HTML entrypoints, here's an example of how to pass a custom option from an HTML file.
|
||||
|
||||
```html [entrypoints/popup.html]
|
||||
<html>
|
||||
<head>
|
||||
<meta name="wxt.myHtmlOption" content="custom value" />
|
||||
<title>Popup</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
#### Generate output file
|
||||
|
||||
```ts
|
||||
@@ -215,7 +301,7 @@ export default defineWxtModule({
|
||||
const analyticsModuleCode = `
|
||||
import { createAnalytics } from 'some-module';
|
||||
|
||||
export const analytics = createAnalytics(useAppConfig().analytics);
|
||||
export const analytics = createAnalytics(getAppConfig().analytics);
|
||||
export const { reportEvent, reportPageView } = analytics;
|
||||
`;
|
||||
|
||||
|
||||
+36
-14
@@ -10,14 +10,14 @@ Run the [`init` command](/api/cli/wxt-init), and follow the instructions.
|
||||
|
||||
:::code-group
|
||||
|
||||
```sh [PNPM]
|
||||
pnpm dlx wxt@latest init
|
||||
```
|
||||
|
||||
```sh [Bun]
|
||||
bunx wxt@latest init
|
||||
```
|
||||
|
||||
```sh [PNPM]
|
||||
pnpm dlx wxt@latest init
|
||||
```
|
||||
|
||||
```sh [NPM]
|
||||
npx wxt@latest init
|
||||
```
|
||||
@@ -45,47 +45,63 @@ Once you've run the `dev` command, continue to [Next Steps](#next-steps)!
|
||||
|
||||
1. Create a new project
|
||||
:::code-group
|
||||
```sh [PNPM]
|
||||
cd my-project
|
||||
pnpm init
|
||||
```
|
||||
|
||||
```sh [Bun]
|
||||
cd my-project
|
||||
bun init
|
||||
```
|
||||
|
||||
```sh [PNPM]
|
||||
cd my-project
|
||||
pnpm init
|
||||
```
|
||||
|
||||
```sh [NPM]
|
||||
cd my-project
|
||||
npm init
|
||||
```
|
||||
|
||||
```sh [Yarn]
|
||||
cd my-project
|
||||
yarn init
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
2. Install WXT:
|
||||
:::code-group
|
||||
|
||||
```sh [Bun]
|
||||
bun add -D wxt
|
||||
```
|
||||
|
||||
```sh [PNPM]
|
||||
pnpm i -D wxt
|
||||
```
|
||||
```sh [Bun]
|
||||
bun i -D wxt
|
||||
```
|
||||
|
||||
```sh [NPM]
|
||||
npm i -D wxt
|
||||
```
|
||||
|
||||
```sh [Yarn]
|
||||
yarn add --dev wxt
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
3. Add an entrypoint, `my-project/entrypoints/background.ts`:
|
||||
:::code-group
|
||||
|
||||
```ts
|
||||
export default defineBackground(() => {
|
||||
console.log('Hello world!');
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
4. Add scripts to your `package.json`:
|
||||
|
||||
```json [package.json]
|
||||
{
|
||||
"scripts": {
|
||||
@@ -99,20 +115,26 @@ Once you've run the `dev` command, continue to [Next Steps](#next-steps)!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. Run your extension in dev mode
|
||||
:::code-group
|
||||
```sh [PNPM]
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
```sh [Bun]
|
||||
bun run dev
|
||||
```
|
||||
|
||||
```sh [PNPM]
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
```sh [NPM]
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```sh [Yarn]
|
||||
yarn dev
|
||||
```
|
||||
|
||||
:::
|
||||
WXT will automatically open a browser window with your extension installed.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Welcome to WXT!
|
||||
# Welcome to WXT
|
||||
|
||||
WXT is a modern, open-source framework for building web extensions. Inspired by Nuxt, its goals are to:
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ This page is dedicated to all the awesome people how have made something for WXT
|
||||
## Blog Posts
|
||||
|
||||
- [Building Modern Cross Browser Web Extensions](https://aabidk.dev/tags/wxt/) by Aabid ([@aabidk20](https://github.com/aabidk20))
|
||||
- [Building Browser Extensions with WXT](https://rxliuli.com/blog/browser-extension-dev-01-introduction-to-basic-concepts/) by rxliuli ([@rxliuli](https://github.com/rxliuli))
|
||||
|
||||
## NPM Packages
|
||||
|
||||
- [`@webext-core/*`](https://webext-core.aklinker1.io/): Easy-to-use utilities for writing and testing web extensions that work on all browsers.
|
||||
- [`Comctx`](https://github.com/molvqingtai/comctx): Cross-context RPC solution with type safety and flexible adapters.
|
||||
- [`wxt-local-analytics`](https://github.com/HaNdTriX/wxt-local-analytics): Local analytics provider for [`@wxt-dev/analytics`](https://wxt.dev/analytics)
|
||||
|
||||
@@ -20,13 +20,13 @@ await chrome.scripting.getRegisteredContentScripts();
|
||||
|
||||
## How do I disable opening the browser automatically during development?
|
||||
|
||||
See https://wxt.dev/guide/essentials/config/browser-startup.html#disable-opening-browser
|
||||
See <https://wxt.dev/guide/essentials/config/browser-startup.html#disable-opening-browser>
|
||||
|
||||
## How do I stay logged into a website during development?
|
||||
|
||||
See https://wxt.dev/guide/essentials/config/browser-startup.html#persist-data
|
||||
See <https://wxt.dev/guide/essentials/config/browser-startup.html#persist-data>
|
||||
|
||||
## My component library doesn't work in content scripts!
|
||||
## My component library doesn't work in content scripts
|
||||
|
||||
This is usually caused by one of two things (or both) when using `createShadowRootUi`:
|
||||
|
||||
@@ -36,7 +36,6 @@ This is usually caused by one of two things (or both) when using `createShadowRo
|
||||
Some component libraries manually add CSS to the page by adding a `<style>` or `<link>` element. They place this element in the document's `<head>` by default. This causes your styles to be placed outside the `ShadowRoot` and it's isolation blocks the styles from being applied to your UI.
|
||||
|
||||
When a library does this, **you need to tell the library where to put its styles**. Here's the documentation for a few popular component libraries:
|
||||
|
||||
- Ant Design: [`StyleProvider`](https://ant.design/docs/react/compatible-style#shadow-dom-usage)
|
||||
- Mantine: [`MantineProvider#getRootElement` and `MantineProvider#cssVariablesSelector`](https://mantine.dev/theming/mantine-provider/)
|
||||
|
||||
@@ -165,12 +164,101 @@ Both issues have the same cause: the library puts something outside the `ShadowR
|
||||
|
||||
Both issues have the same fix: tell the library to put elements inside the `ShadowRoot`, not outside it. See the details above for more information and example fixes for each problem.
|
||||
|
||||
## My content script UI looks different on certain websites
|
||||
|
||||
If your `createShadowRootUi` looks correct on most sites but appears at the wrong size on others (e.g., Reddit), it's because `rem` unit is relative to the `<html>` element's `font-size`, which lives outside the Shadow DOM. When a website overrides it, your UI scales incorrectly.
|
||||
|
||||
The fix is to convert `rem` units to `px` at build time using [`postcss-rem-to-responsive-pixel`](https://www.npmjs.com/package/postcss-rem-to-responsive-pixel). This eliminates the dependency on the host page's root font-size.
|
||||
|
||||
1. Install the package:
|
||||
|
||||
```sh
|
||||
bun i -D postcss-rem-to-responsive-pixel
|
||||
```
|
||||
|
||||
2. Configure your PostCSS config:
|
||||
|
||||
```js [postcss.config.mjs]
|
||||
import remToPx from 'postcss-rem-to-responsive-pixel';
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
remToPx({
|
||||
rootValue: 16,
|
||||
propList: ['*'],
|
||||
transformUnit: 'px',
|
||||
}),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
See [Issue #678](https://github.com/wxt-dev/wxt/issues/678) for additional context on this behavior.
|
||||
|
||||
## Does WXT provide docs for LLMs?
|
||||
|
||||
Yes, WXT's documentation provides markdown files based on the [the /llms.txt proposal](https://llmstxt.org/).
|
||||
|
||||
## Is there an LLM trained on WXT's docs that I chat with?
|
||||
|
||||
Yes! There's a "Ask AI" button in the bottom right of the page, try it out! Or visit https://knowledge.wxt.dev/ for a fullscreen experience.
|
||||
Yes! There's a "Ask AI" button in the bottom right of the page, try it out! Or visit <https://knowledge.wxt.dev/> for a fullscreen experience.
|
||||
|
||||
Additionally, if you want to train your own model or provide context to your editor, you can use the LLM knowledge files hosted by the site:
|
||||
|
||||
https://wxt.dev/knowledge/index.json
|
||||
<https://wxt.dev/knowledge/index.json>
|
||||
|
||||
You don't need to crawl the entire website, these files already contain all the relevant docs for training a LLM on WXT. But feel free to crawl it and generate your own files if you want!
|
||||
|
||||
## How do I run my WXT project with docker / [devcontainers](https://containers.dev)?
|
||||
|
||||
To run the WXT dev server in a devcontainer, but load the dev build of your extension in your browser:
|
||||
|
||||
1. **Bind-mount your project directory to your host**
|
||||
If you're using VS Code, you can open your project folder with the `Dev Containers: Open Folder in Container...` command. This keeps the folder synchronized between your host and the devcontainer, ensuring that the extension `dist` directory remains accessible from the host.
|
||||
|
||||
2. **Disable auto-opening the browser**
|
||||
WXT automatically opens your browser during development, but since you're running inside a container, it won't be able to access it. Follow [the guide here](https://wxt.dev/guide/essentials/config/browser-startup.html#disable-opening-browser) to disable browser auto-opening in your `wxt.config.ts`.
|
||||
|
||||
3. **Tell WXT to listen on all network interfaces**
|
||||
To enable hot-reloading, your extension has to connect to the WXT dev server running inside your container. WXT will only listen on `localhost` by default, which prevents connections from outside the devcontainer. To fix this you can instruct WXT to listen on all interfaces with `wxt --host 0.0.0.0`.
|
||||
|
||||
4. **Enable polling if file changes are not detected**
|
||||
Some container, WSL, and network file system setups do not emit native file events reliably. Configure Chokidar polling in `wxt.config.ts`:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
watchOptions: {
|
||||
usePolling: true,
|
||||
interval: 1000,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## How do I use the new Prompt API in Chrome?
|
||||
|
||||
The service responsible for the [Prompt API](https://developer.chrome.com/docs/ai/prompt-api) is not enabled by default if you let WXT open the browser during dev mode. When checking `LanguageModel.availability`, you will always receive "unavailable".
|
||||
|
||||
You have two options:
|
||||
|
||||
1. Pass the `--disable-features=DisableLoadExtensionCommandLineSwitch` feature flag to enable the service in the browser WXT opens:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
webExt: {
|
||||
chromiumArgs: [
|
||||
'--disable-features=DisableLoadExtensionCommandLineSwitch',
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
2. Disable the runner and install your extension in your regular chrome profile manually:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
webExt: {
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -15,6 +15,11 @@ cd path/to/your/project
|
||||
pnpm dlx wxt@latest init example-wxt --template vanilla
|
||||
```
|
||||
|
||||
:::tip
|
||||
We recommend reviewing [project structure](/guide/essentials/project-structure.md) before you get started.
|
||||
You can customize directory names in `wxt.config.ts` to match your project's needs.
|
||||
:::
|
||||
|
||||
In general, you'll need to:
|
||||
|
||||
 <input type="checkbox" /> Install `wxt`<br />
|
||||
|
||||
@@ -6,12 +6,31 @@ outline: deep
|
||||
|
||||
## Overview
|
||||
|
||||
To upgrade WXT to the latest version... just install it!
|
||||
To upgrade WXT to the latest major version:
|
||||
|
||||
1. Install it, skipping scripts so `wxt prepare` doesn't run - it will probably throw an error after a major version change (we'll run it later).
|
||||
|
||||
```sh
|
||||
pnpm i wxt@latest --ignore-scripts
|
||||
```
|
||||
|
||||
2. Follow the upgrade steps below to fix any breaking changes.
|
||||
3. Run `wxt prepare`. It should succeed and type errors will go away afterwords.
|
||||
|
||||
```sh
|
||||
pnpm wxt prepare
|
||||
```
|
||||
|
||||
4. Manually test to make sure both dev mode and production builds work.
|
||||
|
||||
For minor or patch version updates, there are no special steps. Just update it with your package manager:
|
||||
|
||||
```sh
|
||||
pnpm i wxt@latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Listed below are all the breaking changes you should address when upgrading to a new version of WXT.
|
||||
|
||||
Currently, WXT is in pre-release. This means changes to the second digit, `v0.X`, are considered major and have breaking changes. Once v1 is released, only major version bumps will have breaking changes.
|
||||
@@ -21,7 +40,7 @@ Currently, WXT is in pre-release. This means changes to the second digit, `v0.X`
|
||||
v0.20 is a big release! There are lots of breaking changes because this version is intended to be a release candidate for v1.0. If all goes well, v1.0 will be released with no additional breaking changes.
|
||||
|
||||
:::tip
|
||||
Read through all the changes once before making any code changes.
|
||||
Read through all the changes once before updating your code.
|
||||
:::
|
||||
|
||||
### `webextension-polyfill` Removed
|
||||
@@ -29,19 +48,36 @@ Read through all the changes once before making any code changes.
|
||||
WXT's `browser` no longer uses the `webextension-polyfill`!
|
||||
|
||||
:::details Why?
|
||||
See https://github.com/wxt-dev/wxt/issues/784
|
||||
See <https://github.com/wxt-dev/wxt/issues/784>
|
||||
:::
|
||||
|
||||
To upgrade, you have two options:
|
||||
|
||||
1. **Stop using the polyfill** - No changes necessary, though you may want to do some manual testing to make sure everything continues to work. None of the early testers of this feature reported any runtime issues once they stopped using the polyfill.
|
||||
- If you're already using `extensionApi: "chrome"`, then you don't need to test anything! You're already using the same `browser` object v0.20 provides by default.
|
||||
1. **Stop using the polyfill**
|
||||
- If you're already using `extensionApi: "chrome"`, then you're not using the polyfill and there is nothing to change!
|
||||
- Otherwise there is only one change: `browser.runtime.onMessage` no longer supports using promises to return a response:
|
||||
|
||||
```ts
|
||||
browser.runtime.onMessage.addListener(async () => { // [!code --]
|
||||
const res = await someAsyncWork(); // [!code --]
|
||||
return res; // [!code --]
|
||||
browser.runtime.onMessage.addListener(async (_message, _sender, sendResponse) => { // [!code ++]
|
||||
someAsyncWork().then((res) => { // [!code ++]
|
||||
sendResponse(res); // [!code ++]
|
||||
}); // [!code ++]
|
||||
return true; // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
2. **Continue using the polyfill** - If you want to keep using the polyfill, you can! One less thing to worry about during this upgrade.
|
||||
- Install `webextension-polyfill` and WXT's [new polyfill module](https://www.npmjs.com/package/@wxt-dev/webextension-polyfill):
|
||||
|
||||
```sh
|
||||
pnpm i webextension-polyfill @wxt-dev/webextension-polyfill
|
||||
```
|
||||
|
||||
- Add the WXT module to your config:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/webextension-polyfill'],
|
||||
@@ -91,28 +127,32 @@ The default location for the `public/` and `modules/` directories have changed t
|
||||
|
||||
- If you follow the default folder structure, you don't need to make any changes.
|
||||
- If you set a custom `srcDir`, you have two options:
|
||||
1. Move the your `public/` and `modules/` directories to the project root:
|
||||
```diff
|
||||
<root>/
|
||||
+ modules/
|
||||
+ public/
|
||||
src/
|
||||
components/
|
||||
entrypoints/
|
||||
- modules/
|
||||
- public/
|
||||
utils/
|
||||
wxt.config.ts
|
||||
```
|
||||
2. Keep the folders in the same place and update your project config:
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
publicDir: 'src/public', // [!code ++]
|
||||
modulesDir: 'src/modules', // [!code ++]
|
||||
});
|
||||
1. Move the your `public/` and `modules/` directories to the project root:
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 {rootDir}/
|
||||
📁 modules/ <!-- [!code ++] -->
|
||||
📁 public/ <!-- [!code ++] -->
|
||||
📂 src/
|
||||
📁 components/
|
||||
📁 entrypoints/
|
||||
📁 modules/ <!-- [!code --] -->
|
||||
📁 public/ <!-- [!code --] -->
|
||||
📁 utils/
|
||||
📄 app.config.ts
|
||||
📄 wxt.config.ts
|
||||
```
|
||||
|
||||
2. Keep the folders in the same place and update your project config:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
publicDir: 'src/public', // [!code ++]
|
||||
modulesDir: 'src/modules', // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
### Import Path Changes and `#imports`
|
||||
|
||||
The APIs exported by `wxt/sandbox`, `wxt/client`, or `wxt/storage` have moved to individual exports under the `wxt/utils/*` path.
|
||||
@@ -137,7 +177,7 @@ import { defineContentScript } from 'wxt/sandbox'; // [!code --]
|
||||
import { ContentScriptContext, useAppConfig } from 'wxt/client'; // [!code --]
|
||||
import { storage } from '#imports'; // [!code ++]
|
||||
import { defineContentScript } from '#imports'; // [!code ++]
|
||||
import { ContentScriptContext, useAppConfig } from '#imports'; // [!code ++]
|
||||
import { ContentScriptContext, getAppConfig } from '#imports'; // [!code ++]
|
||||
```
|
||||
|
||||
You can combine the imports into a single import statement, but it's easier to just find/replace each statement.
|
||||
@@ -151,7 +191,7 @@ import {
|
||||
storage, // [!code ++]
|
||||
defineContentScript, // [!code ++]
|
||||
ContentScriptContext, // [!code ++]
|
||||
useAppConfig, // [!code ++]
|
||||
getAppConfig, // [!code ++]
|
||||
} from '#imports'; // [!code ++]
|
||||
```
|
||||
|
||||
@@ -213,6 +253,7 @@ If you've previously loaded the extension into your browser manually for develop
|
||||
### Deprecated APIs Removed
|
||||
|
||||
- `entrypointLoader` option: WXT now uses `vite-node` for importing entrypoints during the build process.
|
||||
<!-- markdownlint-disable-next-line MD051 -->
|
||||
> This was deprecated in v0.19.0, see the [v0.19 section](#v0-18-5-rarr-v0-19-0) for migration steps.
|
||||
- `transformManifest` option: Use the `build:manifestGenerated` hook to transform the manifest instead:
|
||||
<!-- prettier-ignore -->
|
||||
@@ -234,6 +275,7 @@ If you've previously loaded the extension into your browser manually for develop
|
||||
To improve consistency with the `web-ext.config.ts` filename, the "runner" API and config options have been renamed. You can continue using the old names, but they have been deprecated and will be removed in a future version:
|
||||
|
||||
1. The `runner` option has been renamed to `webExt`:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
runner: { // [!code --]
|
||||
@@ -242,12 +284,16 @@ To improve consistency with the `web-ext.config.ts` filename, the "runner" API a
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
2. `defineRunnerConfig` has been renamed to `defineWebExtConfig`:
|
||||
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineRunnerConfig } from 'wxt'; // [!code --]
|
||||
import { defineWebExtConfig } from 'wxt'; // [!code ++]
|
||||
```
|
||||
|
||||
3. The `ExtensionRunnerConfig` type has been renamed to `WebExtConfig`
|
||||
|
||||
```ts
|
||||
import type { ExtensionRunnerConfig } from 'wxt'; // [!code --]
|
||||
import type { WebExtConfig } from 'wxt'; // [!code ++]
|
||||
@@ -325,7 +371,7 @@ WXT no longer ships with Common JS support. If you're using CJS, here's your mig
|
||||
1. Add [`"type": "module"`](https://nodejs.org/api/packages.html#type) to your `package.json`.
|
||||
2. Change the file extension of any `.js` files that use CJS syntax to `.cjs`, or update them to use EMS syntax.
|
||||
|
||||
Vite also provides steps for migrating to ESM. Check them out for more details: https://vitejs.dev/guide/migration#deprecate-cjs-node-api
|
||||
Vite also provides steps for migrating to ESM. Check them out for more details: <https://vitejs.dev/guide/migration#deprecate-cjs-node-api>
|
||||
|
||||
## v0.18.0 → v0.18.5
|
||||
|
||||
@@ -340,11 +386,13 @@ If you already have `<srcDir>/modules` or `<srcDir>/Modules` directory, `wxt pre
|
||||
You have two options:
|
||||
|
||||
1. [Recommended] Keep your files where they are and tell WXT to look in a different folder:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
modulesDir: 'wxt-modules', // defaults to "modules"
|
||||
});
|
||||
```
|
||||
|
||||
2. Rename your `modules` directory to something else.
|
||||
|
||||
## v0.17.0 → v0.18.0
|
||||
@@ -398,7 +446,7 @@ item.watch((newValue: number, oldValue: number) => { // [!code ++]
|
||||
|
||||
JS entrypoints in the output directory have been moved. Unless you're doing some kind of post-build work referencing files, you don't have to make any changes.
|
||||
|
||||
```
|
||||
```plaintext
|
||||
.output/
|
||||
<target>/
|
||||
chunks/
|
||||
@@ -428,7 +476,7 @@ export default defineConfig({
|
||||
|
||||
### Renamed Undocumented Constants
|
||||
|
||||
Renamed undocumented constants for detecting the build config at runtime in [#380](https://github.com/wxt-dev/wxt/pull/380). Now documented here: https://wxt.dev/guide/multiple-browsers.html#runtime
|
||||
Renamed undocumented constants for detecting the build config at runtime in [#380](https://github.com/wxt-dev/wxt/pull/380). Now documented here: <https://wxt.dev/guide/essentials/config/environment-variables.html#built-in-environment-variables>
|
||||
|
||||
- `__BROWSER__` → `import.meta.env.BROWSER`
|
||||
- `__COMMAND__` → `import.meta.env.COMMAND`
|
||||
@@ -455,7 +503,7 @@ Renamed undocumented constants for detecting the build config at runtime in [#38
|
||||
|
||||
### New `wxt/storage` APIs
|
||||
|
||||
`wxt/storage` no longer relies on [`unstorage`](https://www.npmjs.com/package/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. See https://wxt.dev/guide/storage and https://wxt.dev/api/reference/wxt/storage/ for more details ([#300](https://github.com/wxt-dev/wxt/pull/300))
|
||||
`wxt/storage` no longer relies on [`unstorage`](https://www.npmjs.com/package/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. See <https://wxt.dev/guide/storage> and <https://wxt.dev/api/reference/wxt/storage/> for more details ([#300](https://github.com/wxt-dev/wxt/pull/300))
|
||||
|
||||
## v0.11.0 → v0.12.0
|
||||
|
||||
@@ -465,6 +513,7 @@ Renamed undocumented constants for detecting the build config at runtime in [#38
|
||||
|
||||
- If you use auto-imports, no changes are required.
|
||||
- If you have disabled auto-imports, you'll need to manually update your import statements:
|
||||
|
||||
```ts
|
||||
import { defineBackground, defineContentScript } from 'wxt/client'; // [!code --]
|
||||
import { defineBackground, defineContentScript } from 'wxt/sandbox'; // [!code ++]
|
||||
@@ -508,6 +557,8 @@ Unlisted scripts must now `export default defineUnlistedScript(...)`.
|
||||
|
||||
### `BackgroundDefinition` Type
|
||||
|
||||
<!-- cspell:disable -->
|
||||
|
||||
Rename `BackgroundScriptDefintition` to `BackgroundDefinition`.
|
||||
|
||||
## v0.6.0 → v0.7.0
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<!--@include: ../packages/is-background/README.md-->
|
||||
@@ -32,6 +32,8 @@
|
||||
/guide/build-targets.html /guide/multiple-browsers.html
|
||||
/guide/installation.html /get-started/installation.html
|
||||
/guide/introduction.html /get-started/introduction.html
|
||||
/guide/upgrade-guide/wxt /guide/resources/upgrading.html
|
||||
/guide/upgrade-guide/wxt.html /guide/resources/upgrading.html
|
||||
|
||||
# 0.19.0
|
||||
/guide/go-further/entrypoint-side-effects.html /guide/go-further/entrypoint-loaders.html
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="114" height="50" viewBox="0 0 114 50" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path fill="#060B10" d="M0 0h114v51H0z"/><g clip-path="url(#b)" fill="#fff"><path d="M44.445 40.302v-7.695l.153-.16h1.853l.154.16v7.695l-.154.16h-1.853l-.153-.16ZM44.445 17.855v-7.696l.153-.159h1.853l.154.16v7.695l-.154.16h-1.853l-.153-.16ZM38.068 34.892h-.254l-1.275-1.323v-.264l2.388-2.482 1.349.002.18.185v1.4l-2.388 2.482ZM36.54 17.227v-.267l1.274-1.32h.254l2.388 2.479v1.398l-.18.19h-1.349l-2.388-2.48ZM28.154 24.107h10.5l.153.16v1.926l-.153.16h-10.5l-.154-.16v-1.927l.154-.16ZM99.844 24.11l.154.159v1.924l-.154.16H89.19l-.153-.16.77-1.924.154-.16h9.883ZM49 29.24h-1.853l-.153-.16v-4.502c0-.802-.303-1.423-1.234-1.443a24.2 24.2 0 0 0-1.613.025l-.089.092v5.826l-.153.16h-1.853l-.154-.16v-7.695l.154-.16h4.169c1.62 0 2.933 1.364 2.933 3.046v4.81l-.154.159v.002ZM57.974 25.871l-.154.16h-4.79l-.154.16c0 .32.31 1.283 1.546 1.283.463 0 .926-.16 1.082-.481l.154-.16h1.852l.154.16c-.154.962-.926 2.407-3.245 2.407-2.625 0-3.861-1.924-3.861-4.172 0-2.248 1.236-4.171 3.708-4.171 2.472 0 3.708 1.924 3.708 4.171v.643Zm-2.319-1.605c0-.16-.153-1.283-1.39-1.283-1.235 0-1.389 1.124-1.389 1.283l.154.16h2.472l.153-.16ZM62.298 26.833c0 .322.154.481.464.481h1.39l.153.16v1.604l-.154.16h-1.39c-1.389 0-2.625-.643-2.625-2.407v-3.529l-.154-.16H58.9l-.154-.159v-1.605l.154-.16h1.082l.154-.159v-1.443l.154-.16h1.852l.154.16v1.443l.154.16h1.699l.153.16v1.604l-.153.16h-1.7l-.153.159v3.529l.002.002ZM68.015 29.24h-1.853l-.153-.16V18.172l.153-.16h1.853l.154.16v10.907l-.154.16v.002ZM72.186 19.935h-1.852l-.154-.16v-1.604l.154-.16h1.852l.154.16v1.605l-.154.16Zm0 9.305h-1.852l-.154-.16v-7.7l.154-.159h1.852l.154.16v7.7l-.154.16ZM79.449 18.171v1.605l-.154.16h-1.39c-.309 0-.463.159-.463.48v.643l.154.16h1.546l.153.16v1.604l-.153.16h-1.546l-.154.159v5.774l-.153.16h-1.853l-.154-.16v-5.774l-.153-.16h-1.083l-.153-.159v-1.605l.153-.16h1.083l.153-.159v-.643c0-1.764 1.236-2.407 2.626-2.407h1.39l.153.16-.002.002ZM85.163 29.4c-.619 1.605-1.236 2.567-3.398 2.567h-.773l-.154-.16v-1.605l.154-.16h.773c.773 0 .926-.159 1.082-.64v-.16l-2.472-6.257v-1.604l.154-.16h1.39l.153.16 1.853 5.455h.153l1.853-5.455.154-.16h1.39l.153.16v1.604l-2.472 6.417.007-.002Z"/></g><path fill="#fff" d="M0 0v50h15V0z"/><path d="M6.518 45.367v-.774H11v.774H6.518Zm0-1.746c0-.44.098-.784.294-1.032.196-.248.474-.372.834-.372.348 0 .622.13.822.39.2.26.3.628.3 1.104l-.336.018c.004-.608.114-1.062.33-1.362.212-.3.524-.45.936-.45.424 0 .748.146.972.438.22.288.33.686.33 1.194v1.242h-.738v-1.152c0-.268-.05-.488-.15-.66a.498.498 0 0 0-.462-.258c-.224 0-.39.088-.498.264-.108.172-.162.39-.162.654v1.146h-.738V43.68c0-.192-.04-.36-.12-.504a.426.426 0 0 0-.396-.216c-.192 0-.33.068-.414.204a.955.955 0 0 0-.126.51v1.116h-.678v-1.17Zm0-2.925v-.774H9.17c.256 0 .472-.04.648-.12a.88.88 0 0 0 .39-.336.926.926 0 0 0 .126-.486.926.926 0 0 0-.126-.486.845.845 0 0 0-.39-.33 1.497 1.497 0 0 0-.648-.126H6.518v-.75h2.664c.604 0 1.068.146 1.392.438.324.288.486.71.486 1.266 0 .56-.162.984-.486 1.272-.324.288-.788.432-1.392.432H6.518Zm0-5.048v-.774H11v.774H6.518Zm0-2.475V32.4H11v.774H6.518Zm3.75-.546v-2.412H11v2.412h-.732Zm-3.69-3.903v-.773H11v.773H6.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-6.98v-.827l3.684-1.056v.318L6.518 20.43v-.888l3.684-1.122v.318L6.518 17.64v-.798L11 18.18v.81l-4.086 1.254v-.438L11 21.097v.756l-4.482 1.302Zm0-7.385v-.774H11v.774H6.518Zm.06-3.283v-.774H11v.774H6.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-4.7v-.773H11v.774H6.518Zm1.572-.545V6.179h.732V8.65H8.09ZM6.518 6.407v-.774H11v.774H6.518Z" fill="#060B10"/></g><rect x="1" y="1" width="112" height="48" rx="3" stroke="#060B10" stroke-width="2"/><defs><clipPath id="a"><rect width="114" height="50" rx="4" fill="#fff"/></clipPath><clipPath id="b"><path fill="#fff" d="M28 10h72v30.462H28z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="114" height="50" viewBox="0 0 114 50" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><rect width="114" height="50" rx="4" fill="#181A1C"/><path fill="#fff" d="M0 0h114v51H0z"/><g clip-path="url(#b)" fill="#181A1C"><path d="M44.445 40.3v-7.694l.153-.16h1.853l.154.16V40.3l-.154.16h-1.853l-.153-.16ZM44.445 17.854V10.16l.153-.16h1.853l.154.16v7.694l-.154.16h-1.853l-.153-.16ZM38.068 34.89h-.254l-1.275-1.322v-.264l2.388-2.482 1.349.002.18.184v1.4l-2.388 2.483ZM36.54 17.226v-.266l1.274-1.321h.254l2.388 2.48v1.397l-.18.19h-1.349l-2.388-2.48ZM28.154 24.106h10.5l.153.16v1.926l-.153.16h-10.5l-.154-.16v-1.926l.154-.16ZM99.844 24.109l.154.16v1.923l-.154.16H89.19l-.153-.16.77-1.924.154-.16h9.883ZM49 29.24h-1.853l-.153-.16v-4.503c0-.802-.303-1.423-1.234-1.443-.48-.012-1.027 0-1.613.025l-.089.093v5.825l-.153.16h-1.853l-.154-.16v-7.694l.154-.16h4.169c1.62 0 2.933 1.363 2.933 3.045v4.81l-.154.159v.002ZM57.974 25.87l-.154.16h-4.79l-.154.16c0 .32.31 1.283 1.546 1.283.463 0 .926-.16 1.082-.481l.154-.16h1.852l.154.16c-.154.962-.926 2.407-3.245 2.407-2.625 0-3.861-1.924-3.861-4.172 0-2.247 1.236-4.17 3.708-4.17 2.472 0 3.708 1.923 3.708 4.17v.643Zm-2.319-1.604c0-.16-.153-1.284-1.39-1.284-1.235 0-1.389 1.124-1.389 1.284l.154.16h2.472l.153-.16ZM62.298 26.832c0 .322.154.481.464.481h1.39l.153.16v1.604l-.154.16h-1.39c-1.389 0-2.625-.643-2.625-2.407V23.3l-.154-.16H58.9l-.154-.159v-1.604l.154-.16h1.082l.154-.16v-1.442l.154-.16h1.852l.154.16v1.443l.154.16h1.699l.153.159v1.604l-.153.16h-1.7l-.153.16v3.528l.002.002ZM68.015 29.24h-1.853l-.153-.16V18.17l.153-.159h1.853l.154.16v10.906l-.154.16v.002ZM72.186 19.935h-1.852l-.154-.16v-1.604l.154-.16h1.852l.154.16v1.604l-.154.16Zm0 9.304h-1.852l-.154-.16V21.38l.154-.16h1.852l.154.16v7.7l-.154.16ZM79.449 18.17v1.605l-.154.16h-1.39c-.309 0-.463.16-.463.48v.644l.154.16h1.546l.153.159v1.604l-.153.16h-1.546l-.154.16v5.773l-.153.16h-1.853l-.154-.16V23.3l-.153-.16h-1.083l-.153-.159v-1.604l.153-.16h1.083l.153-.16v-.642c0-1.764 1.236-2.407 2.626-2.407h1.39l.153.16-.002.002ZM85.163 29.399c-.619 1.604-1.236 2.566-3.398 2.566h-.773l-.154-.16v-1.604l.154-.16h.773c.773 0 .926-.159 1.082-.64v-.16l-2.472-6.256V21.38l.154-.16h1.39l.153.16 1.853 5.455h.153l1.853-5.455.154-.16h1.39l.153.16v1.605L85.156 29.4l.007-.002Z"/></g><path fill="#181A1C" d="M0 0h15v50H0z"/><path d="M5.518 45.367v-.774H10v.774H5.518Zm0-1.746c0-.44.098-.784.294-1.032.196-.248.474-.372.834-.372.348 0 .622.13.822.39.2.26.3.628.3 1.104l-.336.018c.004-.608.114-1.062.33-1.362.212-.3.524-.45.936-.45.424 0 .748.146.972.438.22.288.33.686.33 1.194v1.242h-.738v-1.152c0-.268-.05-.488-.15-.66a.498.498 0 0 0-.462-.258c-.224 0-.39.088-.498.264-.108.172-.162.39-.162.654v1.146h-.738V43.68c0-.192-.04-.36-.12-.504a.426.426 0 0 0-.396-.216c-.192 0-.33.068-.414.204a.955.955 0 0 0-.126.51v1.116h-.678v-1.17Zm0-2.925v-.774H8.17c.256 0 .472-.04.648-.12a.88.88 0 0 0 .39-.336.926.926 0 0 0 .126-.486.926.926 0 0 0-.126-.486.845.845 0 0 0-.39-.33 1.497 1.497 0 0 0-.648-.126H5.518v-.75h2.664c.604 0 1.068.146 1.392.438.324.288.486.71.486 1.266 0 .56-.162.984-.486 1.272-.324.288-.788.432-1.392.432H5.518Zm0-5.048v-.774H10v.774H5.518Zm0-2.475V32.4H10v.774H5.518Zm3.75-.546v-2.412H10v2.412h-.732Zm-3.69-3.903v-.773H10v.773H5.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-6.98v-.827l3.684-1.056v.318L5.518 20.43v-.888l3.684-1.122v.318L5.518 17.64v-.798L10 18.18v.81l-4.086 1.254v-.438L10 21.097v.756l-4.482 1.302Zm0-7.385v-.774H10v.774H5.518Zm.06-3.283v-.774H10v.774H5.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-4.7v-.773H10v.774H5.518Zm1.572-.545V6.179h.732V8.65H7.09ZM5.518 6.407v-.774H10v.774H5.518Z" fill="#fff"/></g><rect x="1" y="1" width="112" height="48" rx="3" stroke="#181A1C" stroke-width="2"/><defs><clipPath id="a"><rect width="114" height="50" rx="4" fill="#fff"/></clipPath><clipPath id="b"><path fill="#fff" d="M28 10h72v30.46H28z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1 @@
|
||||
<!--@include: ../packages/runner/README.md-->
|
||||
+3
-5
@@ -4,7 +4,7 @@ outline: deep
|
||||
|
||||
# WXT Storage
|
||||
|
||||
[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md)
|
||||
[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md) • [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage)
|
||||
|
||||
A simplified wrapper around the extension storage APIs.
|
||||
|
||||
@@ -74,7 +74,7 @@ await storage.watch<number>(
|
||||
await storage.getMeta<{ v: number }>('local:installDate');
|
||||
```
|
||||
|
||||
For a full list of methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage).
|
||||
> This approach is fine for one-off storage fields or generic helpers, but [defining storage items](#defining-storage-items) is the recommended way to add type-safety.
|
||||
|
||||
## Watchers
|
||||
|
||||
@@ -146,7 +146,7 @@ const showChangelogOnUpdate = storage.defineItem<boolean>(
|
||||
);
|
||||
```
|
||||
|
||||
Now, instead of using the `storage` variable, you can use the helper functions on the storage item you created:
|
||||
Now, instead of using the `storage` variable, you can use the storage item instead:
|
||||
|
||||
```ts
|
||||
await showChangelogOnUpdate.getValue();
|
||||
@@ -157,8 +157,6 @@ const unwatch = showChangelogOnUpdate.watch((newValue) => {
|
||||
});
|
||||
```
|
||||
|
||||
For a full list of properties and methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorageItem).
|
||||
|
||||
### Versioning
|
||||
|
||||
You can add versioning to storage items if you expect them to grow or change over time. When defining the first version of an item, start with version 1.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
Output docs/assets/init-demo.gif
|
||||
|
||||
# The tools we will use
|
||||
Require pnpm
|
||||
Require bun
|
||||
|
||||
|
||||
# === Scene ====
|
||||
@@ -26,7 +26,7 @@ Set FontSize 32
|
||||
# Terminal settings
|
||||
Set Shell "bash"
|
||||
|
||||
# Terminal prompt. It looks like "● mycommand ..."
|
||||
# Terminal prompt. It looks like "● command ..."
|
||||
Env PS1 "\e[0;32m●\e[0m "
|
||||
|
||||
|
||||
@@ -35,19 +35,19 @@ Env PS1 "\e[0;32m●\e[0m "
|
||||
|
||||
Hide
|
||||
# Create a temporary folder for demo
|
||||
Type 'vhs_sandbox="$(mktemp -d)"' Enter
|
||||
Type 'cd "$vhs_sandbox"' Enter
|
||||
Type 'mkdir packages/wxt/init-demo' Enter
|
||||
Type 'cd packages/wxt/init-demo' Enter
|
||||
Type 'clear' Enter
|
||||
Show
|
||||
|
||||
|
||||
# ===== Actions =====
|
||||
|
||||
Type 'pnpm dlx wxt@latest init .' Sleep 1s Enter
|
||||
Type 'bunx wxt init .' Sleep 1s Enter
|
||||
Sleep 3.25s
|
||||
Down@750ms Enter@750ms # Select `vue` template
|
||||
Sleep 1.25s
|
||||
Down@750ms Enter@750ms # Select `pnpm` as a package manager
|
||||
Down@500ms Down@500ms Enter@750ms # Select `pnpm` as a package manager
|
||||
Sleep 5s
|
||||
|
||||
|
||||
@@ -55,5 +55,6 @@ Sleep 5s
|
||||
|
||||
Hide
|
||||
# Delete the temporary folder
|
||||
Type 'rm -rf "$vhs_sandbox"' Enter
|
||||
Type 'cd .."' Enter
|
||||
Type 'rm -rf init-demo' Enter
|
||||
Show
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[build]
|
||||
command = "bun run docs:build"
|
||||
publish = "docs/.vitepress/dist"
|
||||
|
||||
[build.environment]
|
||||
BUN_VERSION = "1.3.5"
|
||||
+63
-45
@@ -2,65 +2,83 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.20.3"
|
||||
"bun": ">=1.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.5.2",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"check": "check && pnpm -r --sequential run check",
|
||||
"test": "pnpm -r --sequential run test run",
|
||||
"test:coverage": "pnpm -r --sequential run test:coverage",
|
||||
"check": "check && bun run --workspaces check",
|
||||
"test": "bun run --workspaces test run",
|
||||
"test:coverage": "bun run --workspaces test:coverage",
|
||||
"prepare": "simple-git-hooks",
|
||||
"prepublish": "pnpm -s build",
|
||||
"docs:gen": "typedoc --options docs/typedoc.json",
|
||||
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
|
||||
"docs:build": "pnpm -s docs:gen && vitepress build docs",
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
|
||||
"docs:dev": "bun run --silent docs:gen && bun run --bun vitepress dev docs",
|
||||
"docs:build": "bun run --silent docs:gen && bun run --bun vitepress build docs",
|
||||
"docs:preview": "bun run --silent docs:gen && bun run --bun vitepress preview docs"
|
||||
},
|
||||
"catalog": {
|
||||
"@aklinker1/buildc": "^1.1.7",
|
||||
"@aklinker1/check": "^2.4.0",
|
||||
"@types/bun": "^1.3.5",
|
||||
"@types/node": "^20.17.6",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@webext-core/fake-browser": "^1.3.4",
|
||||
"nano-spawn": "^2.0.0",
|
||||
"oxlint": "^1.63.0",
|
||||
"publint": "^0.3.18",
|
||||
"tsdown": "^0.21.0",
|
||||
"typescript": "^6.0.3",
|
||||
"unocss": "^66.6.8",
|
||||
"vitest": "^4.1.5",
|
||||
"vitest-plugin-random-seed": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "catalog:",
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@commitlint/config-conventional": "catalog:",
|
||||
"@commitlint/types": "catalog:",
|
||||
"@types/fs-extra": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"changelogen": "catalog:",
|
||||
"consola": "catalog:",
|
||||
"fast-glob": "catalog:",
|
||||
"feed": "catalog:",
|
||||
"fs-extra": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
"markdown-it-footnote": "catalog:",
|
||||
"@commitlint/config-conventional": "^21.2.0",
|
||||
"@commitlint/types": "^21.2.0",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@vitest/coverage-v8": "^4.1.6",
|
||||
"changelogen": "^0.6.2",
|
||||
"consola": "^3.4.2",
|
||||
"cspell": "^9.7.0",
|
||||
"feed": "^5.2.1",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"markdownlint-cli": "^0.49.0",
|
||||
"nano-spawn": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"simple-git-hooks": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"typedoc": "catalog:",
|
||||
"typedoc-plugin-frontmatter": "catalog:",
|
||||
"typedoc-plugin-markdown": "catalog:",
|
||||
"typedoc-vitepress-theme": "catalog:",
|
||||
"nano-staged": "^1.0.2",
|
||||
"p-map": "^7.0.4",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-jsdoc": "^1.8.0",
|
||||
"semver": "^7.8.0",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"tinyglobby": "^0.2.16",
|
||||
"tsdown": "catalog:",
|
||||
"tsx": "4.21.0",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-frontmatter": "^1.3.1",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "catalog:",
|
||||
"vitepress": "catalog:",
|
||||
"vitepress-knowledge": "catalog:",
|
||||
"vitepress-plugin-group-icons": "catalog:",
|
||||
"vitest-mock-extended": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vitepress": "^1.6.4",
|
||||
"vitepress-knowledge": "^0.4.1",
|
||||
"vitepress-plugin-group-icons": "^1.7.3",
|
||||
"vitepress-plugin-llms": "^1.12.2",
|
||||
"vitest-mock-extended": "^4.0.0",
|
||||
"vue": "^3.5.34",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"markdown-it-footnote@4.0.0": "patches/markdown-it-footnote.patch",
|
||||
"simple-git-hooks@2.13.1": "patches/simple-git-hooks@2.13.1.patch"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
"pre-commit": "./node_modules/.bin/nano-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"nano-staged": {
|
||||
"*": "prettier --ignore-unknown --write"
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
"@algolia/client-search",
|
||||
"search-insights"
|
||||
]
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"markdown-it-footnote": "patches/markdown-it-footnote.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Changelog
|
||||
|
||||
## v0.5.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.4...analytics-v0.5.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **analytics:** Add Moderok analytics provider ([#2393](https://github.com/wxt-dev/wxt/pull/2393))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **analytics:** Change method return types from void to Promise<void> ([#2409](https://github.com/wxt-dev/wxt/pull/2409))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add `prettier-plugin-jsdoc` to project ([#2171](https://github.com/wxt-dev/wxt/pull/2171))
|
||||
- **deps:** Upgrade deps ([#2175](https://github.com/wxt-dev/wxt/pull/2175))
|
||||
- Add prepack script to all packages ([032f7931](https://github.com/wxt-dev/wxt/commit/032f7931))
|
||||
- Use `deps.neverBundle` instead of `external` ([8f71e174](https://github.com/wxt-dev/wxt/commit/8f71e174))
|
||||
- Migrate monorepo to use Bun instead of PNPM ([#2009](https://github.com/wxt-dev/wxt/pull/2009))
|
||||
- **deps-dev:** Bump typescript from 5.9.3 to 6.0.3 ([#2325](https://github.com/wxt-dev/wxt/pull/2325))
|
||||
- Use `catalog:` for dev dependencies ([#2357](https://github.com/wxt-dev/wxt/pull/2357))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Muhammed Mustafa AKŞAM ([@muhammedaksam](https://github.com/muhammedaksam))
|
||||
- Vaughn Bosu ([@VaughnBosu](https://github.com/VaughnBosu))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.5.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.3...analytics-v0.5.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Continue using `useAppConfig` to support older versions of WXT ([bfd94556](https://github.com/wxt-dev/wxt/commit/bfd94556))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.5.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.2...analytics-v0.5.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add `getAppConfig` as an alias to `useAppConfig` ([#2144](https://github.com/wxt-dev/wxt/pull/2144))
|
||||
- Allow `userId` option to return `undefined` ([636cf1f8](https://github.com/wxt-dev/wxt/commit/636cf1f8))
|
||||
- Improve background script detection logic for analytics package ([#1808](https://github.com/wxt-dev/wxt/pull/1808))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Created new types, instead of `any` for `analytics` ([#2119](https://github.com/wxt-dev/wxt/pull/2119))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Smit ([@sm17p](https://github.com/sm17p))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
|
||||
- Sheng Zhang ([@Arktomson](https://github.com/Arktomson))
|
||||
|
||||
## v0.5.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.1...analytics-v0.5.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Normalize path for createAnalytics of analytics/index.ts ([#2013](https://github.com/wxt-dev/wxt/pull/2013))
|
||||
- Allow custom API URL in Google Analytics 4 provider options ([#1653](https://github.com/wxt-dev/wxt/pull/1653))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Code cleanup in analytics package ([#2084](https://github.com/wxt-dev/wxt/pull/2084))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Fix other type error after `chrome` types update ([31ebf966](https://github.com/wxt-dev/wxt/commit/31ebf966))
|
||||
- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))
|
||||
- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))
|
||||
- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))
|
||||
- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Honwhy Wang <honwhy.wang@gmail.com>
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
|
||||
|
||||
## v0.5.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.0...analytics-v0.5.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Integrate latest measurement protocol changes ([#1767](https://github.com/wxt-dev/wxt/pull/1767))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use `@wxt-dev/browser` instead of `@types/chrome` ([#1645](https://github.com/wxt-dev/wxt/pull/1645))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))
|
||||
- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))
|
||||
- Change browser workspace dependency to `^` ([c7335add](https://github.com/wxt-dev/wxt/commit/c7335add))
|
||||
- Fix auto-fixable `markdownlint` errors ([#1710](https://github.com/wxt-dev/wxt/pull/1710))
|
||||
- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))
|
||||
- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))
|
||||
- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Tanishq-aggarwal ([@tanishq-aggarwal](https://github.com/tanishq-aggarwal))
|
||||
|
||||
## v0.5.0
|
||||
|
||||
[⚠️ breaking changes](https://wxt.dev/guide/resources/upgrading.html) • [compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.4.1...analytics-v0.5.0)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- ⚠️ Update min WXT version to 0.20 ([2e8baf0](https://github.com/wxt-dev/wxt/commit/2e8baf0))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
@@ -5,20 +5,25 @@ Report analytics events from your web extension extension.
|
||||
## Supported Analytics Providers
|
||||
|
||||
- [Google Analytics 4 (Measurement Protocol)](#google-analytics-4-measurement-protocol)
|
||||
- [Moderok](#moderok)
|
||||
- [Umami](#umami)
|
||||
|
||||
## Install With WXT
|
||||
|
||||
1. Install the NPM package:
|
||||
|
||||
```bash
|
||||
pnpm i @wxt-dev/analytics
|
||||
```
|
||||
|
||||
2. In your `wxt.config.ts`, add the WXT module:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/analytics/module'],
|
||||
});
|
||||
```
|
||||
|
||||
3. In your `<srcDir>/app.config.ts`, add a provider:
|
||||
|
||||
```ts
|
||||
@@ -49,9 +54,11 @@ Report analytics events from your web extension extension.
|
||||
## Install Without WXT
|
||||
|
||||
1. Install the NPM package:
|
||||
|
||||
```bash
|
||||
pnpm i @wxt-dev/analytics
|
||||
```
|
||||
|
||||
2. Create an `analytics` instance:
|
||||
|
||||
```ts
|
||||
@@ -66,10 +73,12 @@ Report analytics events from your web extension extension.
|
||||
```
|
||||
|
||||
3. Import your analytics module in the background to initialize the message listener:
|
||||
|
||||
```ts
|
||||
// background.ts
|
||||
import './utils/analytics';
|
||||
```
|
||||
|
||||
4. Then use your `analytics` instance to report events:
|
||||
|
||||
```ts
|
||||
@@ -112,6 +121,41 @@ export default defineAppConfig({
|
||||
});
|
||||
```
|
||||
|
||||
### Moderok
|
||||
|
||||
[Moderok](https://moderok.dev) is an analytics platform built specifically for browser extensions. It requires no `host_permissions`, works in Manifest V3 service workers, and collects only anonymous usage data.
|
||||
|
||||
Sign up at [moderok.dev](https://moderok.dev) to get your app key, then save it to your `.env` file:
|
||||
|
||||
```dotenv
|
||||
WXT_MODEROK_APP_KEY='mk_...'
|
||||
```
|
||||
|
||||
Then add the `moderok` provider to your `<srcDir>/app.config.ts` file:
|
||||
|
||||
```ts
|
||||
import { moderok } from '@wxt-dev/analytics/providers/moderok';
|
||||
|
||||
export default defineAppConfig({
|
||||
analytics: {
|
||||
providers: [
|
||||
moderok({
|
||||
appKey: import.meta.env.WXT_MODEROK_APP_KEY,
|
||||
// Automatically track first open, install, update, and daily ping events (default: true)
|
||||
trackLifecycle: true,
|
||||
// Track when users uninstall the extension (default: false)
|
||||
trackUninstalls: false,
|
||||
// Optional: when trackUninstalls is on, redirect users to this page
|
||||
// after they uninstall (e.g. a feedback survey)
|
||||
uninstallUrl: 'https://example.com/uninstall',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
For a full walkthrough — module setup, sending events, and all provider options — see the [Moderok WXT guide](https://docs.moderok.dev/guide/wxt).
|
||||
|
||||
### Umami
|
||||
|
||||
[Umami](https://umami.is/) is a privacy-first, open source analytics platform.
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
// Build module and plugins
|
||||
export default defineBuildConfig({
|
||||
rootDir: resolve(__dirname, 'modules/analytics'),
|
||||
outDir: resolve(__dirname, 'dist'),
|
||||
entries: [
|
||||
{ input: 'index.ts', name: 'module' },
|
||||
{ input: 'client.ts', name: 'index' },
|
||||
'background-plugin.ts',
|
||||
'types.ts',
|
||||
'providers/google-analytics-4.ts',
|
||||
'providers/umami.ts',
|
||||
],
|
||||
externals: ['#analytics'],
|
||||
replace: {
|
||||
'import.meta.env.NPM': 'true',
|
||||
},
|
||||
declaration: true,
|
||||
});
|
||||
@@ -2,18 +2,51 @@ import { UAParser } from 'ua-parser-js';
|
||||
import type {
|
||||
Analytics,
|
||||
AnalyticsConfig,
|
||||
AnalyticsEventMetadata,
|
||||
AnalyticsPageViewEvent,
|
||||
AnalyticsProvider,
|
||||
AnalyticsStorageItem,
|
||||
AnalyticsTrackEvent,
|
||||
BaseAnalyticsEvent,
|
||||
AnalyticsEventMetadata,
|
||||
AnalyticsProvider,
|
||||
} from './types';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
import { isBackground } from '@wxt-dev/is-background';
|
||||
|
||||
type AnalyticsMessage = {
|
||||
[K in keyof Analytics]: {
|
||||
fn: K;
|
||||
args: Parameters<Analytics[K]>;
|
||||
};
|
||||
}[keyof Analytics];
|
||||
|
||||
type AnalyticsMethod =
|
||||
| ((...args: Parameters<Analytics[keyof Analytics]>) => void)
|
||||
| undefined;
|
||||
|
||||
type MethodForwarder = <K extends keyof Analytics>(
|
||||
fn: K,
|
||||
) => (...args: Parameters<Analytics[K]>) => Promise<void>;
|
||||
|
||||
const ANALYTICS_PORT = '@wxt-dev/analytics';
|
||||
|
||||
const INTERACTIVE_TAGS = new Set([
|
||||
'A',
|
||||
'BUTTON',
|
||||
'INPUT',
|
||||
'SELECT',
|
||||
'TEXTAREA',
|
||||
]);
|
||||
const INTERACTIVE_ROLES = new Set([
|
||||
'button',
|
||||
'link',
|
||||
'checkbox',
|
||||
'menuitem',
|
||||
'tab',
|
||||
'radio',
|
||||
]);
|
||||
|
||||
export function createAnalytics(config?: AnalyticsConfig): Analytics {
|
||||
if (typeof chrome === 'undefined' || !chrome?.runtime?.id)
|
||||
if (!browser?.runtime?.id)
|
||||
throw Error(
|
||||
'Cannot use WXT analytics in contexts without access to the browser.runtime APIs',
|
||||
);
|
||||
@@ -23,16 +56,14 @@ export function createAnalytics(config?: AnalyticsConfig): Analytics {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: This only works for standard WXT extensions, add a more generic
|
||||
// background script detector that works with non-WXT projects.
|
||||
if (location.pathname === '/background.js')
|
||||
return createBackgroundAnalytics(config);
|
||||
if (isBackground()) return createBackgroundAnalytics(config);
|
||||
|
||||
return createFrontendAnalytics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an analytics client in the background responsible for uploading events to the server to avoid CORS errors.
|
||||
* Creates an analytics client in the background responsible for uploading
|
||||
* events to the server to avoid CORS errors.
|
||||
*/
|
||||
function createBackgroundAnalytics(
|
||||
config: AnalyticsConfig | undefined,
|
||||
@@ -51,18 +82,22 @@ function createBackgroundAnalytics(
|
||||
defineStorageItem<boolean>('local:wxt-analytics:enabled', false);
|
||||
|
||||
// Cached values
|
||||
const platformInfo = chrome.runtime.getPlatformInfo();
|
||||
const platformInfo = browser.runtime.getPlatformInfo();
|
||||
const userAgent = UAParser();
|
||||
let userId = Promise.resolve(userIdStorage.getValue()).then(
|
||||
(id) => id ?? globalThis.crypto.randomUUID(),
|
||||
);
|
||||
let userId = Promise.resolve(userIdStorage.getValue()).then(async (id) => {
|
||||
if (id != null) return id;
|
||||
// Persist the generated ID so it's stable across service worker restarts.
|
||||
const generatedId = globalThis.crypto.randomUUID();
|
||||
await userIdStorage.setValue?.(generatedId);
|
||||
return generatedId;
|
||||
});
|
||||
let userProperties = userPropertiesStorage.getValue();
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const manifest = browser.runtime.getManifest();
|
||||
|
||||
const getBackgroundMeta = () => ({
|
||||
timestamp: Date.now(),
|
||||
// Don't track sessions for the background, it can be running
|
||||
// indefinitely, and will inflate session duration stats.
|
||||
// Don't track sessions for the background, it can be running indefinitely
|
||||
// and will inflate session duration stats.
|
||||
sessionId: undefined,
|
||||
language: navigator.language,
|
||||
referrer: undefined,
|
||||
@@ -74,7 +109,7 @@ function createBackgroundAnalytics(
|
||||
const getBaseEvent = async (
|
||||
meta: AnalyticsEventMetadata,
|
||||
): Promise<BaseAnalyticsEvent> => {
|
||||
const platform = await platformInfo;
|
||||
const { arch, os } = await platformInfo;
|
||||
return {
|
||||
meta,
|
||||
user: {
|
||||
@@ -83,8 +118,8 @@ function createBackgroundAnalytics(
|
||||
version: config?.version ?? manifest.version_name ?? manifest.version,
|
||||
wxtMode: import.meta.env.MODE,
|
||||
wxtBrowser: import.meta.env.BROWSER,
|
||||
arch: platform.arch,
|
||||
os: platform.os,
|
||||
arch,
|
||||
os,
|
||||
browser: userAgent.browser.name,
|
||||
browserVersion: userAgent.browser.version,
|
||||
...(await userProperties),
|
||||
@@ -146,7 +181,7 @@ function createBackgroundAnalytics(
|
||||
},
|
||||
track: async (
|
||||
eventName: string,
|
||||
eventProperties?: Record<string, string>,
|
||||
eventProperties?: Record<string, string | undefined>,
|
||||
meta: AnalyticsEventMetadata = getBackgroundMeta(),
|
||||
) => {
|
||||
const baseEvent = await getBaseEvent(meta);
|
||||
@@ -178,11 +213,10 @@ function createBackgroundAnalytics(
|
||||
config?.providers?.map((provider) => provider(analytics, config)) ?? [];
|
||||
|
||||
// Listen for messages from the rest of the extension
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
browser.runtime.onConnect.addListener((port) => {
|
||||
if (port.name === ANALYTICS_PORT) {
|
||||
port.onMessage.addListener(({ fn, args }) => {
|
||||
// @ts-expect-error: Untyped fn key
|
||||
void analytics[fn]?.(...args);
|
||||
port.onMessage.addListener(({ fn, args }: AnalyticsMessage) => {
|
||||
void (analytics[fn] as AnalyticsMethod)?.(...args);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -190,28 +224,25 @@ function createBackgroundAnalytics(
|
||||
return analytics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an analytics client for non-background contexts.
|
||||
*/
|
||||
/** Creates an analytics client for non-background contexts. */
|
||||
function createFrontendAnalytics(): Analytics {
|
||||
const port = chrome.runtime.connect({ name: ANALYTICS_PORT });
|
||||
const port = browser.runtime.connect({ name: ANALYTICS_PORT });
|
||||
const sessionId = Date.now();
|
||||
const getFrontendMetadata = (): AnalyticsEventMetadata => ({
|
||||
sessionId,
|
||||
timestamp: Date.now(),
|
||||
language: navigator.language,
|
||||
referrer: globalThis.document?.referrer || undefined,
|
||||
screen: globalThis.window
|
||||
? `${globalThis.window.screen.width}x${globalThis.window.screen.height}`
|
||||
: undefined,
|
||||
referrer: document.referrer || undefined,
|
||||
screen: `${window.screen.width}x${window.screen.height}`,
|
||||
url: location.href,
|
||||
title: document.title || undefined,
|
||||
});
|
||||
|
||||
const methodForwarder =
|
||||
(fn: string) =>
|
||||
(...args: any[]) => {
|
||||
const methodForwarder: MethodForwarder =
|
||||
(fn) =>
|
||||
(...args) => {
|
||||
port.postMessage({ fn, args: [...args, getFrontendMetadata()] });
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const analytics: Analytics = {
|
||||
@@ -221,11 +252,11 @@ function createFrontendAnalytics(): Analytics {
|
||||
setEnabled: methodForwarder('setEnabled'),
|
||||
autoTrack: (root) => {
|
||||
const onClick = (event: Event) => {
|
||||
const element = event.target as any;
|
||||
const element = event.target as HTMLElement | null;
|
||||
if (
|
||||
!element ||
|
||||
(!INTERACTIVE_TAGS.has(element.tagName) &&
|
||||
!INTERACTIVE_ROLES.has(element.getAttribute('role')))
|
||||
!INTERACTIVE_ROLES.has(element.getAttribute('role') ?? ''))
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -234,7 +265,7 @@ function createFrontendAnalytics(): Analytics {
|
||||
id: element.id || undefined,
|
||||
className: element.className || undefined,
|
||||
textContent: element.textContent?.substring(0, 50) || undefined, // Limit text content length
|
||||
href: element.href,
|
||||
href: (element as HTMLAnchorElement).href,
|
||||
});
|
||||
};
|
||||
root.addEventListener('click', onClick, { capture: true, passive: true });
|
||||
@@ -246,33 +277,23 @@ function createFrontendAnalytics(): Analytics {
|
||||
return analytics;
|
||||
}
|
||||
|
||||
function defineStorageItem<T>(key: string): AnalyticsStorageItem<T | undefined>;
|
||||
function defineStorageItem<T>(
|
||||
key: string,
|
||||
defaultValue?: NonNullable<T>,
|
||||
): AnalyticsStorageItem<T> {
|
||||
defaultValue: T,
|
||||
): AnalyticsStorageItem<T>;
|
||||
function defineStorageItem(
|
||||
key: string,
|
||||
defaultValue?: unknown,
|
||||
): AnalyticsStorageItem<unknown> {
|
||||
return {
|
||||
getValue: async () =>
|
||||
(await chrome.storage.local.get(key))[key] ?? defaultValue,
|
||||
setValue: (newValue) => chrome.storage.local.set({ [key]: newValue }),
|
||||
(await browser.storage.local.get<Record<string, unknown>>(key))[key] ??
|
||||
defaultValue,
|
||||
setValue: (newValue) => browser.storage.local.set({ [key]: newValue }),
|
||||
};
|
||||
}
|
||||
|
||||
const INTERACTIVE_TAGS = new Set([
|
||||
'A',
|
||||
'BUTTON',
|
||||
'INPUT',
|
||||
'SELECT',
|
||||
'TEXTAREA',
|
||||
]);
|
||||
const INTERACTIVE_ROLES = new Set([
|
||||
'button',
|
||||
'link',
|
||||
'checkbox',
|
||||
'menuitem',
|
||||
'tab',
|
||||
'radio',
|
||||
]);
|
||||
|
||||
export function defineAnalyticsProvider<T = never>(
|
||||
definition: (
|
||||
/** The analytics object. */
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'wxt/modules';
|
||||
import { relative, resolve } from 'node:path';
|
||||
import type { AnalyticsConfig } from './types';
|
||||
import { normalizePath } from 'wxt';
|
||||
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
@@ -22,10 +23,10 @@ export default defineWxtModule({
|
||||
// Paths
|
||||
const wxtAnalyticsFolder = resolve(wxt.config.wxtDir, 'analytics');
|
||||
const wxtAnalyticsIndex = resolve(wxtAnalyticsFolder, 'index.ts');
|
||||
const clientModuleId = import.meta.env.NPM
|
||||
const clientModuleId = process.env.NPM
|
||||
? '@wxt-dev/analytics'
|
||||
: resolve(wxt.config.modulesDir, 'analytics/client');
|
||||
const pluginModuleId = import.meta.env.NPM
|
||||
const pluginModuleId = process.env.NPM
|
||||
? '@wxt-dev/analytics/background-plugin'
|
||||
: resolve(wxt.config.modulesDir, 'analytics/background-plugin');
|
||||
|
||||
@@ -38,17 +39,15 @@ export default defineWxtModule({
|
||||
});
|
||||
|
||||
// Generate #analytics module
|
||||
const wxtAnalyticsCode = [
|
||||
`import { createAnalytics } from '${
|
||||
import.meta.env.NPM
|
||||
? clientModuleId
|
||||
: relative(wxtAnalyticsFolder, clientModuleId)
|
||||
}';`,
|
||||
`import { useAppConfig } from '#imports';`,
|
||||
``,
|
||||
`export const analytics = createAnalytics(useAppConfig().analytics);`,
|
||||
``,
|
||||
].join('\n');
|
||||
const wxtAnalyticsCode = `import { createAnalytics } from '${
|
||||
process.env.NPM
|
||||
? clientModuleId
|
||||
: normalizePath(relative(wxtAnalyticsFolder, clientModuleId))
|
||||
}';
|
||||
import { useAppConfig } from '#imports';
|
||||
|
||||
export const analytics = createAnalytics(useAppConfig().analytics);
|
||||
`;
|
||||
addAlias(wxt, '#analytics', wxtAnalyticsIndex);
|
||||
wxt.hook('prepare:types', async (_, entries) => {
|
||||
entries.push({
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { BaseAnalyticsEvent } from '../types';
|
||||
const DEFAULT_ENGAGEMENT_TIME_IN_MSEC = 100;
|
||||
|
||||
export interface GoogleAnalytics4ProviderOptions {
|
||||
apiUrl?: string;
|
||||
apiSecret: string;
|
||||
measurementId: string;
|
||||
}
|
||||
@@ -18,7 +19,7 @@ export const googleAnalytics4 =
|
||||
): Promise<void> => {
|
||||
const url = new URL(
|
||||
config?.debug ? '/debug/mp/collect' : '/mp/collect',
|
||||
'https://www.google-analytics.com',
|
||||
options.apiUrl ?? 'https://www.google-analytics.com',
|
||||
);
|
||||
if (options.apiSecret)
|
||||
url.searchParams.set('api_secret', options.apiSecret);
|
||||
@@ -46,6 +47,7 @@ export const googleAnalytics4 =
|
||||
ad_personalization: 'DENIED',
|
||||
},
|
||||
user_properties: mappedUserProperties,
|
||||
user_agent: navigator.userAgent,
|
||||
events: [
|
||||
{
|
||||
name: eventName,
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import { defineAnalyticsProvider } from '../client';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
import type { BaseAnalyticsEvent } from '../types';
|
||||
|
||||
const SDK_VERSION = 'wxt/0.1.0';
|
||||
const DEFAULT_ENDPOINT = 'https://api.moderok.dev/v1/events';
|
||||
const PING_STORAGE_KEY = 'moderok:last-ping-date';
|
||||
const FIRST_OPEN_STORAGE_KEY = 'moderok:first-open';
|
||||
|
||||
export interface ModerokProviderOptions {
|
||||
appKey: string;
|
||||
endpoint?: string;
|
||||
trackLifecycle?: boolean;
|
||||
trackUninstalls?: boolean;
|
||||
uninstallUrl?: string;
|
||||
}
|
||||
|
||||
const OS_MAP: Record<string, string> = {
|
||||
mac: 'MacOS',
|
||||
win: 'Windows',
|
||||
linux: 'Linux',
|
||||
cros: 'ChromeOS',
|
||||
android: 'Android',
|
||||
};
|
||||
|
||||
const BROWSER_MAP: Record<string, string> = {
|
||||
chrome: 'chrome',
|
||||
edge: 'edge',
|
||||
firefox: 'firefox',
|
||||
chromium: 'other_chromium',
|
||||
};
|
||||
|
||||
function mapOs(wxtOs: string | undefined): string {
|
||||
if (!wxtOs) return 'unknown';
|
||||
return OS_MAP[wxtOs.toLowerCase()] ?? 'unknown';
|
||||
}
|
||||
|
||||
function mapBrowser(wxtBrowser: string | undefined): string {
|
||||
if (!wxtBrowser) return 'unknown';
|
||||
return BROWSER_MAP[wxtBrowser.toLowerCase()] ?? 'unknown';
|
||||
}
|
||||
|
||||
function detectSource(meta: BaseAnalyticsEvent['meta']): string {
|
||||
if (meta.sessionId == null) return 'background';
|
||||
|
||||
const url = meta.url;
|
||||
if (!url) return 'unknown';
|
||||
const isExtensionUrl =
|
||||
url.startsWith('chrome-extension://') || url.startsWith('moz-extension://');
|
||||
if (!isExtensionUrl) return 'content_script';
|
||||
|
||||
const path = url.toLowerCase();
|
||||
if (path.includes('popup')) return 'popup';
|
||||
if (path.includes('option')) return 'options';
|
||||
if (
|
||||
path.includes('sidepanel') ||
|
||||
path.includes('side-panel') ||
|
||||
path.includes('side_panel')
|
||||
)
|
||||
return 'side_panel';
|
||||
|
||||
return 'extension_page';
|
||||
}
|
||||
|
||||
function buildContext(event: BaseAnalyticsEvent, extensionId: string) {
|
||||
return {
|
||||
sdkVersion: SDK_VERSION,
|
||||
extensionId: extensionId || 'unknown',
|
||||
extensionVersion: event.user.properties.version || 'unknown',
|
||||
browser: mapBrowser(event.user.properties.browser),
|
||||
browserVersion: event.user.properties.browserVersion || 'unknown',
|
||||
os: mapOs(event.user.properties.os),
|
||||
locale: event.meta.language || 'unknown',
|
||||
source: detectSource(event.meta),
|
||||
};
|
||||
}
|
||||
|
||||
function sendEvent(
|
||||
endpoint: string,
|
||||
appKey: string,
|
||||
event: {
|
||||
name: string;
|
||||
userId: string;
|
||||
timestamp: number;
|
||||
context: ReturnType<typeof buildContext>;
|
||||
properties?: Record<string, string | number | boolean>;
|
||||
},
|
||||
debug: boolean,
|
||||
) {
|
||||
const payload = {
|
||||
appKey,
|
||||
sentAt: Date.now(),
|
||||
events: [
|
||||
{
|
||||
id: globalThis.crypto.randomUUID(),
|
||||
name: event.name,
|
||||
timestamp: event.timestamp,
|
||||
userId: event.userId,
|
||||
context: event.context,
|
||||
...(event.properties && Object.keys(event.properties).length > 0
|
||||
? { properties: event.properties }
|
||||
: {}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (debug) {
|
||||
console.debug('[@wxt-dev/analytics][moderok] Sending:', payload);
|
||||
}
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
function cleanProperties(
|
||||
raw: Record<string, string | undefined> | undefined,
|
||||
): Record<string, string> | undefined {
|
||||
if (!raw) return undefined;
|
||||
const clean: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
if (value != null) clean[key] = value;
|
||||
}
|
||||
return Object.keys(clean).length > 0 ? clean : undefined;
|
||||
}
|
||||
|
||||
function utcDateStamp(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function buildUninstallUrl(
|
||||
endpoint: string,
|
||||
appKey: string,
|
||||
userId: string,
|
||||
redirectUrl?: string,
|
||||
): string {
|
||||
const url = new URL(endpoint);
|
||||
url.pathname = url.pathname.replace(/\/[^/]*$/, '/uninstall');
|
||||
url.searchParams.set('app', appKey);
|
||||
url.searchParams.set('uid', userId);
|
||||
if (redirectUrl) url.searchParams.set('redirect', redirectUrl);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export const moderok = defineAnalyticsProvider<ModerokProviderOptions>(
|
||||
(analytics, config, options) => {
|
||||
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
||||
const debug = config.debug ?? false;
|
||||
const trackLifecycle = options.trackLifecycle ?? true;
|
||||
const extensionId = browser.runtime.id ?? '';
|
||||
let firstOpenPromise: Promise<void> | undefined;
|
||||
|
||||
const maybeTrackFirstOpen = async (event: BaseAnalyticsEvent) => {
|
||||
if (!trackLifecycle) return;
|
||||
if (firstOpenPromise) return firstOpenPromise;
|
||||
|
||||
firstOpenPromise = (async () => {
|
||||
const stored = await browser.storage.local.get(FIRST_OPEN_STORAGE_KEY);
|
||||
if (stored[FIRST_OPEN_STORAGE_KEY]) return;
|
||||
|
||||
const response = await sendEvent(
|
||||
endpoint,
|
||||
options.appKey,
|
||||
{
|
||||
name: '__first_open',
|
||||
userId: event.user.id,
|
||||
timestamp: event.meta.timestamp,
|
||||
context: buildContext(event, extensionId),
|
||||
},
|
||||
debug,
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
await browser.storage.local.set({ [FIRST_OPEN_STORAGE_KEY]: true });
|
||||
} else {
|
||||
firstOpenPromise = undefined;
|
||||
}
|
||||
})().catch((error) => {
|
||||
firstOpenPromise = undefined;
|
||||
throw error;
|
||||
});
|
||||
|
||||
return firstOpenPromise;
|
||||
};
|
||||
|
||||
if (trackLifecycle) {
|
||||
browser.runtime.onInstalled.addListener((details) => {
|
||||
if (details.reason === 'install') {
|
||||
analytics.track('__install');
|
||||
} else if (details.reason === 'update') {
|
||||
analytics.track('__update', {
|
||||
previousVersion: (details as { previousVersion?: string })
|
||||
.previousVersion,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
void (async () => {
|
||||
const today = utcDateStamp();
|
||||
const stored = await browser.storage.local.get(PING_STORAGE_KEY);
|
||||
if (stored[PING_STORAGE_KEY] === today) return;
|
||||
await browser.storage.local.set({ [PING_STORAGE_KEY]: today });
|
||||
analytics.track('__daily_ping');
|
||||
})();
|
||||
}
|
||||
|
||||
let uninstallUrlSet = false;
|
||||
function maybeSetUninstallUrl(userId: string) {
|
||||
if (!options.trackUninstalls || uninstallUrlSet) return;
|
||||
uninstallUrlSet = true;
|
||||
|
||||
const url = buildUninstallUrl(
|
||||
endpoint,
|
||||
options.appKey,
|
||||
userId,
|
||||
options.uninstallUrl,
|
||||
);
|
||||
if (url.length <= 1023) {
|
||||
browser.runtime.setUninstallURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
identify: () => Promise.resolve(),
|
||||
|
||||
page: async (event) => {
|
||||
maybeSetUninstallUrl(event.user.id);
|
||||
await maybeTrackFirstOpen(event);
|
||||
await sendEvent(
|
||||
endpoint,
|
||||
options.appKey,
|
||||
{
|
||||
name: '__page_view',
|
||||
userId: event.user.id,
|
||||
timestamp: event.meta.timestamp,
|
||||
context: buildContext(event, extensionId),
|
||||
properties: {
|
||||
url: event.page.url,
|
||||
...(event.page.title ? { title: event.page.title } : {}),
|
||||
...(event.page.location ? { location: event.page.location } : {}),
|
||||
},
|
||||
},
|
||||
debug,
|
||||
);
|
||||
},
|
||||
|
||||
track: async (event) => {
|
||||
maybeSetUninstallUrl(event.user.id);
|
||||
await maybeTrackFirstOpen(event);
|
||||
await sendEvent(
|
||||
endpoint,
|
||||
options.appKey,
|
||||
{
|
||||
name: event.event.name.trim(),
|
||||
userId: event.user.id,
|
||||
timestamp: event.meta.timestamp,
|
||||
context: buildContext(event, extensionId),
|
||||
properties: cleanProperties(event.event.properties),
|
||||
},
|
||||
debug,
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -1,40 +1,49 @@
|
||||
export interface Analytics {
|
||||
/** Report a page change. */
|
||||
page: (url: string) => void;
|
||||
page: (url: string) => Promise<void>;
|
||||
/** Report a custom event. */
|
||||
track: (eventName: string, eventProperties?: Record<string, string>) => void;
|
||||
track: (
|
||||
eventName: string,
|
||||
eventProperties?: Record<string, string | undefined>,
|
||||
) => Promise<void>;
|
||||
/** Save information about the user. */
|
||||
identify: (userId: string, userProperties?: Record<string, string>) => void;
|
||||
/** Automatically setup and track user interactions, returning a function to remove any listeners that were setup. */
|
||||
identify: (
|
||||
userId: string,
|
||||
userProperties?: Record<string, string>,
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* Automatically setup and track user interactions, returning a function to
|
||||
* remove any listeners that were setup.
|
||||
*/
|
||||
autoTrack: (root: Document | ShadowRoot | Element) => () => void;
|
||||
/** Calls `config.enabled.setValue`. */
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
setEnabled: (enabled: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface AnalyticsConfig {
|
||||
/**
|
||||
* Array of providers to send analytics to.
|
||||
*/
|
||||
/** Array of providers to send analytics to. */
|
||||
providers: AnalyticsProvider[];
|
||||
/**
|
||||
* Enable debug logs and other provider-specific debugging features.
|
||||
*/
|
||||
/** Enable debug logs and other provider-specific debugging features. */
|
||||
debug?: boolean;
|
||||
/**
|
||||
* Your extension's version, reported alongside events.
|
||||
*
|
||||
* @default browser.runtime.getManifest().version`.
|
||||
*/
|
||||
version?: string;
|
||||
/**
|
||||
* Configure how the enabled flag is persisted. Defaults to using `browser.storage.local`.
|
||||
* Configure how the enabled flag is persisted. Defaults to using
|
||||
* `browser.storage.local`.
|
||||
*/
|
||||
enabled?: AnalyticsStorageItem<boolean>;
|
||||
/**
|
||||
* Configure how the user Id is persisted. Defaults to using `browser.storage.local`.
|
||||
* Configure how the user Id is persisted. Defaults to using
|
||||
* `browser.storage.local`.
|
||||
*/
|
||||
userId?: AnalyticsStorageItem<string>;
|
||||
userId?: AnalyticsStorageItem<string | undefined>;
|
||||
/**
|
||||
* Configure how user properties are persisted. Defaults to using `browser.storage.local`.
|
||||
* Configure how user properties are persisted. Defaults to using
|
||||
* `browser.storage.local`.
|
||||
*/
|
||||
userProperties?: AnalyticsStorageItem<Record<string, string>>;
|
||||
}
|
||||
@@ -94,6 +103,6 @@ export interface AnalyticsPageViewEvent extends BaseAnalyticsEvent {
|
||||
export interface AnalyticsTrackEvent extends BaseAnalyticsEvent {
|
||||
event: {
|
||||
name: string;
|
||||
properties?: Record<string, string>;
|
||||
properties?: Record<string, string | undefined>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,40 @@
|
||||
{
|
||||
"name": "@wxt-dev/analytics",
|
||||
"version": "0.4.1",
|
||||
"version": "0.5.5",
|
||||
"description": "Add analytics to your web extension",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "buildc --deps-only -- wxt",
|
||||
"dev:build": "buildc --deps-only -- wxt build",
|
||||
"check": "bun run build && check",
|
||||
"build": "buildc -- tsdown",
|
||||
"prepack": "bun run build",
|
||||
"postinstall": "buildc --deps-only -- wxt prepare",
|
||||
"test": "echo 'noop'",
|
||||
"test:coverage": "echo 'noop'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wxt-dev/browser": "workspace:^",
|
||||
"@wxt-dev/is-background": "workspace:^",
|
||||
"ua-parser-js": "^1.0.40"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "catalog:",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"publint": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/analytics"
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
@@ -29,37 +55,18 @@
|
||||
"types": "./dist/providers/google-analytics-4.d.mts",
|
||||
"default": "./dist/providers/google-analytics-4.mjs"
|
||||
},
|
||||
"./providers/moderok": {
|
||||
"types": "./dist/providers/moderok.d.mts",
|
||||
"default": "./dist/providers/moderok.mjs"
|
||||
},
|
||||
"./providers/umami": {
|
||||
"types": "./dist/providers/umami.d.mts",
|
||||
"default": "./dist/providers/umami.mjs"
|
||||
}
|
||||
},
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"types": "./dist/index.d.mts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "buildc --deps-only -- wxt",
|
||||
"dev:build": "buildc --deps-only -- wxt build",
|
||||
"check": "pnpm build && check",
|
||||
"build": "buildc -- unbuild",
|
||||
"prepack": "pnpm -s build",
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@types/chrome": "catalog:",
|
||||
"@types/ua-parser-js": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"ua-parser-js": "catalog:"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"#analytics": ["./.wxt/analytics/index.ts"]
|
||||
},
|
||||
"types": ["chrome"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: './modules/analytics/client.ts',
|
||||
module: './modules/analytics/index.ts',
|
||||
'background-plugin': './modules/analytics/background-plugin.ts',
|
||||
types: './modules/analytics/types.ts',
|
||||
'providers/google-analytics-4':
|
||||
'./modules/analytics/providers/google-analytics-4.ts',
|
||||
'providers/umami': './modules/analytics/providers/umami.ts',
|
||||
'providers/moderok': './modules/analytics/providers/moderok.ts',
|
||||
},
|
||||
deps: {
|
||||
neverBundle: ['#analytics'],
|
||||
},
|
||||
define: {
|
||||
'process.env.NPM': 'true',
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,66 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.1.0...auto-icons-v1.1.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Auto icons override default icons ([#1616](https://github.com/wxt-dev/wxt/pull/1616))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Standardize file existence checks to `pathExists` ([#2083](https://github.com/wxt-dev/wxt/pull/2083))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))
|
||||
- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))
|
||||
- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))
|
||||
- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))
|
||||
- **deps:** Upgrade non-breaking production dependencies ([#1877](https://github.com/wxt-dev/wxt/pull/1877))
|
||||
- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))
|
||||
- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))
|
||||
- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))
|
||||
- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Omerfardemir <od080624@gmail.com>
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v1.1.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.0.2...auto-icons-v1.1.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add overlay option for dev icons ([#1825](https://github.com/wxt-dev/wxt/pull/1825))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Rewrite and restructure the documentation website ([#933](https://github.com/wxt-dev/wxt/pull/933))
|
||||
- Use full URLs in README so they work on the docs site ([d20793d5](https://github.com/wxt-dev/wxt/commit/d20793d5))
|
||||
- Add SVG compatibility note ([#1830](https://github.com/wxt-dev/wxt/pull/1830))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))
|
||||
- **deps:** Bump sharp from 0.33.4 to 0.33.5 ([#959](https://github.com/wxt-dev/wxt/pull/959))
|
||||
- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))
|
||||
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
|
||||
- **deps:** Bump dev and non-breaking major dependencies ([#1167](https://github.com/wxt-dev/wxt/pull/1167))
|
||||
- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))
|
||||
- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))
|
||||
- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))
|
||||
- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Typed SIGTERM ([@typed-sigterm](https://github.com/typed-sigterm))
|
||||
- Kuba ([@zizzfizzix](https://github.com/zizzfizzix))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v1.0.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.0.1...auto-icons-v1.0.2)
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
## Features
|
||||
|
||||
- Generate extension icons with the correct sizes
|
||||
- Make the icon greyscale during development
|
||||
- Make the icon greyscale or include a visible overlay during development
|
||||
- SVG is supported
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -15,7 +16,7 @@ Install the package:
|
||||
npm i --save-dev @wxt-dev/auto-icons
|
||||
pnpm i -D @wxt-dev/auto-icons
|
||||
yarn add --dev @wxt-dev/auto-icons
|
||||
bun i -D @wxt-dev/auto-icons
|
||||
bun add -D @wxt-dev/auto-icons
|
||||
```
|
||||
|
||||
Add the module to `wxt.config.ts`:
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.1",
|
||||
"type": "module",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"types": "./dist/index.d.mts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
@@ -38,23 +38,26 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "pnpm build && check"
|
||||
"build": "buildc -- tsdown",
|
||||
"check": "bun run build && check",
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
"test:coverage": "bun run test run --coverage",
|
||||
"prepack": "bun run build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"defu": "catalog:",
|
||||
"fs-extra": "catalog:",
|
||||
"sharp": "catalog:"
|
||||
"defu": "^6.1.4",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@aklinker1/buildc": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,630 @@
|
||||
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||
import { resolve } from 'node:path';
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import type { Wxt, UserManifest } from 'wxt';
|
||||
|
||||
// Import the actual module
|
||||
import autoIconsModule from '../index';
|
||||
import type { AutoIconsOptions } from '../index';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
mkdir: vi.fn(),
|
||||
access: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('sharp', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
// Type definitions for better type safety
|
||||
interface MockWxt {
|
||||
config: {
|
||||
srcDir: string;
|
||||
outDir: string;
|
||||
mode: 'development' | 'production';
|
||||
};
|
||||
logger: {
|
||||
warn: Mock;
|
||||
};
|
||||
hooks: {
|
||||
hook: Mock;
|
||||
};
|
||||
}
|
||||
|
||||
interface PublicAsset {
|
||||
type: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
interface BuildOutput {
|
||||
publicAssets: PublicAsset[];
|
||||
}
|
||||
|
||||
describe('auto-icons module', () => {
|
||||
const mockWxt: MockWxt = {
|
||||
config: {
|
||||
srcDir: '/mock/src',
|
||||
outDir: '/mock/dist',
|
||||
mode: 'development',
|
||||
},
|
||||
logger: {
|
||||
warn: vi.fn(),
|
||||
},
|
||||
hooks: {
|
||||
hook: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const createMockSharpInstance = () => {
|
||||
const instance = {
|
||||
png: vi.fn(),
|
||||
grayscale: vi.fn(),
|
||||
resize: vi.fn(),
|
||||
toFile: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
// Make methods chainable
|
||||
instance.png.mockReturnValue(instance);
|
||||
instance.grayscale.mockReturnValue(instance);
|
||||
instance.resize.mockImplementation(() => {
|
||||
// Create a new instance for each resize to simulate real sharp behavior
|
||||
const resizedInstance = { ...instance };
|
||||
resizedInstance.toFile = vi.fn().mockResolvedValue(undefined);
|
||||
return resizedInstance;
|
||||
});
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
let mockSharpInstance: ReturnType<typeof createMockSharpInstance>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSharpInstance = createMockSharpInstance();
|
||||
vi.mocked(sharp).mockReturnValue(
|
||||
mockSharpInstance as unknown as sharp.Sharp,
|
||||
);
|
||||
vi.mocked(fsPromises.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fsPromises.mkdir).mockResolvedValue(undefined as any);
|
||||
});
|
||||
|
||||
describe('module setup', () => {
|
||||
it('should have correct module metadata', () => {
|
||||
expect(autoIconsModule.name).toBe('@wxt-dev/auto-icons');
|
||||
expect(autoIconsModule.configKey).toBe('autoIcons');
|
||||
expect(typeof autoIconsModule.setup).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('options handling', () => {
|
||||
it('should use default options when not provided', async () => {
|
||||
const options: AutoIconsOptions = {};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
// Verify that the module was set up (hooks were registered)
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'build:manifestGenerated',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'build:done',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'prepare:publicPaths',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge custom options with defaults', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
sizes: [64, 32],
|
||||
grayscaleOnDevelopment: false,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
// Verify that the module was set up with custom options
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'build:manifestGenerated',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'build:done',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'prepare:publicPaths',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should warn when disabled', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
|
||||
'`[auto-icons]` @wxt-dev/auto-icons disabled',
|
||||
);
|
||||
expect(mockWxt.hooks.hook).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should warn when base icon not found', async () => {
|
||||
vi.mocked(fsPromises.access).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
baseIconPath: 'assets/missing-icon.png',
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Skipping icon generation, no base icon found at',
|
||||
),
|
||||
);
|
||||
expect(mockWxt.hooks.hook).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('manifest generation hook', () => {
|
||||
it('should update manifest with default icons when no custom sizes provided', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const manifestHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
|
||||
|
||||
expect(manifestHook).toBeDefined();
|
||||
|
||||
const manifest: UserManifest = {};
|
||||
if (manifestHook) {
|
||||
await manifestHook(mockWxt as unknown as Wxt, manifest);
|
||||
}
|
||||
|
||||
// Should use default sizes: [128, 48, 32, 16]
|
||||
expect(manifest.icons).toEqual({
|
||||
128: 'icons/128.png',
|
||||
48: 'icons/48.png',
|
||||
32: 'icons/32.png',
|
||||
16: 'icons/16.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge custom sizes with defaults', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
sizes: [96, 64], // These will be merged with defaults
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const manifestHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
|
||||
|
||||
expect(manifestHook).toBeDefined();
|
||||
|
||||
const manifest: UserManifest = {};
|
||||
if (manifestHook) {
|
||||
await manifestHook(mockWxt as unknown as Wxt, manifest);
|
||||
}
|
||||
|
||||
// defu merges arrays, so we get both custom and default sizes
|
||||
expect(manifest.icons).toEqual({
|
||||
96: 'icons/96.png',
|
||||
64: 'icons/64.png',
|
||||
128: 'icons/128.png',
|
||||
48: 'icons/48.png',
|
||||
32: 'icons/32.png',
|
||||
16: 'icons/16.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should warn when overwriting existing icons in manifest', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const manifestHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
|
||||
|
||||
const manifest: UserManifest = {
|
||||
icons: {
|
||||
128: 'existing-icon.png',
|
||||
},
|
||||
};
|
||||
|
||||
if (manifestHook) {
|
||||
await manifestHook(mockWxt as unknown as Wxt, manifest);
|
||||
}
|
||||
|
||||
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
|
||||
'`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('icon generation hook', () => {
|
||||
it('should generate icons with default sizes', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const output: BuildOutput = {
|
||||
publicAssets: [],
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
expect(buildHook).toBeDefined();
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
expect(sharp).toHaveBeenCalledWith(
|
||||
resolve('/mock/src', 'assets/icon.png'),
|
||||
);
|
||||
expect(mockSharpInstance.png).toHaveBeenCalled();
|
||||
|
||||
// Should resize to default sizes
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(128);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(48);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);
|
||||
|
||||
expect(fsPromises.mkdir).toHaveBeenCalledWith(
|
||||
resolve('/mock/dist', 'icons'),
|
||||
{ recursive: true },
|
||||
);
|
||||
|
||||
expect(output.publicAssets).toEqual([
|
||||
{ type: 'asset', fileName: 'icons/128.png' },
|
||||
{ type: 'asset', fileName: 'icons/48.png' },
|
||||
{ type: 'asset', fileName: 'icons/32.png' },
|
||||
{ type: 'asset', fileName: 'icons/16.png' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate icons with custom sizes merged with defaults', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
sizes: [96, 64],
|
||||
};
|
||||
|
||||
const output: BuildOutput = {
|
||||
publicAssets: [],
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
expect(buildHook).toBeDefined();
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
// Should include both custom and default sizes
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(96);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(64);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(128);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(48);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);
|
||||
|
||||
expect(output.publicAssets).toEqual([
|
||||
{ type: 'asset', fileName: 'icons/96.png' },
|
||||
{ type: 'asset', fileName: 'icons/64.png' },
|
||||
{ type: 'asset', fileName: 'icons/128.png' },
|
||||
{ type: 'asset', fileName: 'icons/48.png' },
|
||||
{ type: 'asset', fileName: 'icons/32.png' },
|
||||
{ type: 'asset', fileName: 'icons/16.png' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should apply grayscale in development mode', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
grayscaleOnDevelopment: true,
|
||||
sizes: [128],
|
||||
};
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
expect(mockSharpInstance.grayscale).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not apply grayscale in production mode', async () => {
|
||||
const prodMockWxt = {
|
||||
...mockWxt,
|
||||
config: {
|
||||
...mockWxt.config,
|
||||
mode: 'production' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
grayscaleOnDevelopment: true,
|
||||
sizes: [128],
|
||||
};
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
await autoIconsModule.setup!(prodMockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(prodMockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
if (buildHook) {
|
||||
await buildHook(prodMockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not apply grayscale when disabled', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
grayscaleOnDevelopment: false,
|
||||
sizes: [128],
|
||||
};
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('public paths hook', () => {
|
||||
it('should add default icon paths to public paths', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const pathsHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1];
|
||||
|
||||
expect(pathsHook).toBeDefined();
|
||||
|
||||
const paths: string[] = [];
|
||||
if (pathsHook) {
|
||||
pathsHook(mockWxt as unknown as Wxt, paths);
|
||||
}
|
||||
|
||||
expect(paths).toEqual([
|
||||
'icons/128.png',
|
||||
'icons/48.png',
|
||||
'icons/32.png',
|
||||
'icons/16.png',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases and error handling', () => {
|
||||
it('should handle empty sizes array', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
sizes: [], // Empty array should still merge with defaults
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const manifestHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
|
||||
|
||||
const manifest: UserManifest = {};
|
||||
if (manifestHook) {
|
||||
await manifestHook(mockWxt as unknown as Wxt, manifest);
|
||||
}
|
||||
|
||||
// Should still have default sizes due to defu merge
|
||||
expect(manifest.icons).toEqual({
|
||||
128: 'icons/128.png',
|
||||
48: 'icons/48.png',
|
||||
32: 'icons/32.png',
|
||||
16: 'icons/16.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle sharp processing errors gracefully', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// Make toFile throw an error - need to properly chain resize -> png -> toFile
|
||||
const errorInstance = {
|
||||
toFile: vi.fn().mockRejectedValue(new Error('Sharp processing failed')),
|
||||
grayscale: vi.fn(),
|
||||
composite: vi.fn(),
|
||||
};
|
||||
errorInstance.grayscale.mockReturnValue(errorInstance);
|
||||
errorInstance.composite.mockReturnValue(errorInstance);
|
||||
|
||||
mockSharpInstance.resize = vi.fn().mockImplementation(() => ({
|
||||
png: vi.fn().mockReturnValue(errorInstance),
|
||||
}));
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
// Should throw the sharp error
|
||||
if (buildHook) {
|
||||
await expect(
|
||||
buildHook(mockWxt as unknown as Wxt, output),
|
||||
).rejects.toThrow('Sharp processing failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle file system errors during directory creation', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// Make ensureDir throw an error
|
||||
vi.mocked(fsPromises.mkdir).mockRejectedValue(
|
||||
new Error('Directory creation failed'),
|
||||
);
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
// The module doesn't await ensureDir, so it won't throw
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
// But ensureDir should have been called
|
||||
expect(fsPromises.mkdir).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle custom base icon path correctly', async () => {
|
||||
const customPath = 'custom/icon.png';
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
baseIconPath: customPath,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
// Should resolve the path relative to srcDir
|
||||
expect(sharp).toHaveBeenCalledWith(resolve('/mock/src', customPath));
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration test', () => {
|
||||
it('should handle full workflow correctly', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
baseIconPath: 'assets/custom-icon.png',
|
||||
sizes: [96], // Will be merged with defaults
|
||||
grayscaleOnDevelopment: false,
|
||||
};
|
||||
|
||||
const manifest: UserManifest = {};
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
const paths: string[] = [];
|
||||
|
||||
// Setup the module
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
// Execute all hooks
|
||||
const manifestHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
const pathsHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1];
|
||||
|
||||
if (manifestHook) {
|
||||
await manifestHook(mockWxt as unknown as Wxt, manifest);
|
||||
}
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
if (pathsHook) {
|
||||
pathsHook(mockWxt as unknown as Wxt, paths);
|
||||
}
|
||||
|
||||
// Verify results - defu merges arrays
|
||||
expect(manifest.icons).toEqual({
|
||||
96: 'icons/96.png',
|
||||
128: 'icons/128.png',
|
||||
48: 'icons/48.png',
|
||||
32: 'icons/32.png',
|
||||
16: 'icons/16.png',
|
||||
});
|
||||
|
||||
expect(output.publicAssets).toEqual([
|
||||
{ type: 'asset', fileName: 'icons/96.png' },
|
||||
{ type: 'asset', fileName: 'icons/128.png' },
|
||||
{ type: 'asset', fileName: 'icons/48.png' },
|
||||
{ type: 'asset', fileName: 'icons/32.png' },
|
||||
{ type: 'asset', fileName: 'icons/16.png' },
|
||||
]);
|
||||
|
||||
expect(paths).toEqual([
|
||||
'icons/96.png',
|
||||
'icons/128.png',
|
||||
'icons/48.png',
|
||||
'icons/32.png',
|
||||
'icons/16.png',
|
||||
]);
|
||||
|
||||
expect(sharp).toHaveBeenCalledWith(
|
||||
resolve('/mock/src', 'assets/custom-icon.png'),
|
||||
);
|
||||
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { defineWxtModule } from 'wxt/modules';
|
||||
import { resolve, relative } from 'node:path';
|
||||
import defu from 'defu';
|
||||
import sharp from 'sharp';
|
||||
import { ensureDir, exists } from 'fs-extra';
|
||||
import { access, mkdir } from 'node:fs/promises';
|
||||
|
||||
export default defineWxtModule<AutoIconsOptions>({
|
||||
name: '@wxt-dev/auto-icons',
|
||||
@@ -14,17 +14,34 @@ export default defineWxtModule<AutoIconsOptions>({
|
||||
{
|
||||
enabled: true,
|
||||
baseIconPath: resolve(wxt.config.srcDir, 'assets/icon.png'),
|
||||
grayscaleOnDevelopment: true,
|
||||
developmentIndicator: 'grayscale',
|
||||
sizes: [128, 48, 32, 16],
|
||||
},
|
||||
);
|
||||
|
||||
// Backward compatibility for the deprecated option
|
||||
if (options?.grayscaleOnDevelopment !== undefined) {
|
||||
wxt.logger.warn(
|
||||
'`[auto-icons]` "grayscaleOnDevelopment" is deprecated. Use "developmentIndicator" instead.',
|
||||
);
|
||||
|
||||
if (options?.developmentIndicator === undefined) {
|
||||
parsedOptions.developmentIndicator = options!.grayscaleOnDevelopment
|
||||
? 'grayscale'
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedPath = resolve(wxt.config.srcDir, parsedOptions.baseIconPath);
|
||||
|
||||
if (!parsedOptions.enabled)
|
||||
return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`);
|
||||
|
||||
if (!(await exists(resolvedPath))) {
|
||||
const iconExists = await access(resolvedPath).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
if (!iconExists) {
|
||||
return wxt.logger.warn(
|
||||
`\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`,
|
||||
);
|
||||
@@ -32,7 +49,7 @@ export default defineWxtModule<AutoIconsOptions>({
|
||||
|
||||
wxt.hooks.hook('build:manifestGenerated', async (wxt, manifest) => {
|
||||
if (manifest.icons)
|
||||
return wxt.logger.warn(
|
||||
wxt.logger.warn(
|
||||
'`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',
|
||||
);
|
||||
|
||||
@@ -42,21 +59,44 @@ export default defineWxtModule<AutoIconsOptions>({
|
||||
});
|
||||
|
||||
wxt.hooks.hook('build:done', async (wxt, output) => {
|
||||
const image = sharp(resolvedPath).png();
|
||||
|
||||
if (
|
||||
wxt.config.mode === 'development' &&
|
||||
parsedOptions.grayscaleOnDevelopment
|
||||
) {
|
||||
image.grayscale();
|
||||
}
|
||||
|
||||
const outputFolder = wxt.config.outDir;
|
||||
|
||||
for (const size of parsedOptions.sizes) {
|
||||
const resized = image.resize(size);
|
||||
ensureDir(resolve(outputFolder, 'icons'));
|
||||
await resized.toFile(resolve(outputFolder, `icons/${size}.png`));
|
||||
const resizedImage = sharp(resolvedPath).resize(size).png();
|
||||
|
||||
if (wxt.config.mode === 'development') {
|
||||
if (parsedOptions.developmentIndicator === 'grayscale') {
|
||||
resizedImage.grayscale();
|
||||
} else if (parsedOptions.developmentIndicator === 'overlay') {
|
||||
// Helper to build an overlay that places a yellow rectangle at the bottom
|
||||
// of the icon with the text "DEV" in black. The overlay has the same
|
||||
// dimensions as the icon so we can composite it with default gravity.
|
||||
const buildDevOverlay = (size: number) => {
|
||||
const rectHeight = Math.round(size * 0.5);
|
||||
const fontSize = Math.round(size * 0.35);
|
||||
|
||||
return Buffer.from(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0" y="${size - rectHeight}" width="${size}" height="${rectHeight}" fill="#ffff00" />
|
||||
<text x="${size / 2}" y="${size - rectHeight / 2}" font-family="Arial, Helvetica, sans-serif" font-size="${fontSize}" font-weight="bold" fill="black" text-anchor="middle" dominant-baseline="middle">DEV</text>
|
||||
</svg>`);
|
||||
};
|
||||
const overlayBuffer = await sharp(buildDevOverlay(size))
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
resizedImage.composite([
|
||||
{
|
||||
input: overlayBuffer,
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
mkdir(resolve(outputFolder, 'icons'), { recursive: true });
|
||||
await resizedImage.toFile(resolve(outputFolder, `icons/${size}.png`));
|
||||
|
||||
output.publicAssets.push({
|
||||
type: 'asset',
|
||||
@@ -73,12 +113,11 @@ export default defineWxtModule<AutoIconsOptions>({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Options for the auto-icons module
|
||||
*/
|
||||
/** Options for the auto-icons module */
|
||||
export interface AutoIconsOptions {
|
||||
/**
|
||||
* Enable auto-icons generation
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
enabled?: boolean;
|
||||
@@ -86,16 +125,30 @@ export interface AutoIconsOptions {
|
||||
* Path to the image to use.
|
||||
*
|
||||
* Path is relative to the project's src directory.
|
||||
* @default "<srcDir>/assets/icon.png"
|
||||
*
|
||||
* @default '<srcDir>/assets/icon.png'
|
||||
*/
|
||||
baseIconPath?: string;
|
||||
/**
|
||||
* Apply a visual indicator to the icon when running in development mode.
|
||||
*
|
||||
* "grayscale" converts the icon to grayscale. "overlay" covers the bottom
|
||||
* half with a yellow rectangle and writes "DEV" in black text. Set to `false`
|
||||
* to disable any indicator.
|
||||
*
|
||||
* @default 'grayscale'
|
||||
*/
|
||||
developmentIndicator?: 'grayscale' | 'overlay' | false;
|
||||
/**
|
||||
* Grayscale the image when in development mode to indicate development
|
||||
*
|
||||
* @deprecated Use `developmentIndicator` instead
|
||||
* @default true
|
||||
*/
|
||||
grayscaleOnDevelopment?: boolean;
|
||||
/**
|
||||
* Sizes to generate icons for
|
||||
*
|
||||
* @default [128, 48, 32, 16]
|
||||
*/
|
||||
sizes?: number[];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user