Compare commits

..

2 Commits

Author SHA1 Message Date
Diego Perez
0939cab8f5 Merge branch 'main' into derek/settingsPageLag 2026-01-08 18:31:24 -06:00
f77f663fe7 feat: made a handler for github stats messages same way as the rest 2025-11-17 15:37:01 -06:00
77 changed files with 157 additions and 2123 deletions

View File

@@ -94,5 +94,3 @@ typings/
config
.eslintrc.js
!.storybook
src/lib/chrome-extension-toolkit

View File

@@ -205,7 +205,7 @@ module.exports = {
{
target: './src/shared',
from: './',
except: ['./src/shared', './node_modules', './src/lib/chrome-extension-toolkit'],
except: ['./src/shared', './node_modules'],
message: 'You cannot import into `shared` from an external directory.',
},
],
@@ -227,6 +227,6 @@ module.exports = {
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'import-essentials/restrict-import-depth': 'error',
// 'import-essentials/check-path-alias': 'error',
'import-essentials/check-path-alias': 'error',
},
};

View File

@@ -23,7 +23,6 @@ export default defineConfig({
'@shared': resolve(root, 'shared'),
'@background': resolve(pagesDir, 'background'),
'@views': resolve(root, 'views'),
'@chrome-extension-toolkit': resolve(root, 'lib/chrome-extension-toolkit'),
},
},
});

View File

@@ -44,6 +44,7 @@
"@tsparticles/slim": "^3.9.1",
"@unocss/vite": "^0.63.6",
"@vitejs/plugin-react": "^4.3.4",
"chrome-extension-toolkit": "^0.0.54",
"clsx": "^2.1.1",
"conventional-changelog": "^6.0.0",
"date-fns": "^4.1.0",
@@ -136,7 +137,6 @@
"gulp": "^5.0.0",
"gulp-execa": "^7.0.1",
"gulp-zip": "^6.1.0",
"jsdom": "^28.0.0",
"path": "^0.12.7",
"postcss": "^8.5.3",
"prettier": "3.6.2",

367
pnpm-lock.yaml generated
View File

@@ -64,6 +64,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.3.4(vite@5.4.20(@types/node@22.13.5)(sass@1.85.1)(terser@5.44.0))
chrome-extension-toolkit:
specifier: ^0.0.54
version: 0.0.54
clsx:
specifier: ^2.1.1
version: 2.1.1
@@ -335,9 +338,6 @@ importers:
gulp-zip:
specifier: ^6.1.0
version: 6.1.0(gulp@5.0.0)
jsdom:
specifier: ^28.0.0
version: 28.0.0
path:
specifier: ^0.12.7
version: 0.12.7
@@ -376,13 +376,10 @@ importers:
version: 0.8.9(rollup@4.52.4)(vite@5.4.20(@types/node@22.13.5)(sass@1.85.1)(terser@5.44.0))
vitest:
specifier: ^2.1.9
version: 2.1.9(@types/node@22.13.5)(@vitest/ui@2.1.9)(happy-dom@20.5.0)(jsdom@28.0.0)(sass@1.85.1)(terser@5.44.0)
version: 2.1.9(@types/node@22.13.5)(@vitest/ui@2.1.9)(sass@1.85.1)(terser@5.44.0)
packages:
'@acemir/cssom@0.9.31':
resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==}
'@adobe/css-tools@4.4.2':
resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==}
@@ -402,15 +399,6 @@ packages:
'@antfu/utils@8.1.1':
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
'@asamuzakjp/css-color@4.1.2':
resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==}
'@asamuzakjp/dom-selector@6.7.8':
resolution: {integrity: sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ==}
'@asamuzakjp/nwsapi@2.3.9':
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
'@babel/code-frame@7.26.2':
resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
engines: {node: '>=6.9.0'}
@@ -627,37 +615,6 @@ packages:
resolution: {integrity: sha512-kSXgHHqCXASqJ8NmY94+KLGVwdtkJ0E7KsRQ+vbMpRliJ5ze0xnSk0l41p4txlUysmEoqaeo4Xb7rEFdcU2zjQ==}
deprecated: Beta versions are no longer maintained. Please upgrade to the stable 2.0.0 release
'@csstools/color-helpers@6.0.1':
resolution: {integrity: sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==}
engines: {node: '>=20.19.0'}
'@csstools/css-calc@3.0.0':
resolution: {integrity: sha512-q4d82GTl8BIlh/dTnVsWmxnbWJeb3kiU8eUH71UxlxnS+WIaALmtzTL8gR15PkYOexMQYVk0CO4qIG93C1IvPA==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@csstools/css-parser-algorithms': ^4.0.0
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-color-parser@4.0.1':
resolution: {integrity: sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@csstools/css-parser-algorithms': ^4.0.0
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-parser-algorithms@4.0.0':
resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-syntax-patches-for-csstree@1.0.26':
resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==}
'@csstools/css-tokenizer@4.0.0':
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
engines: {node: '>=20.19.0'}
'@date-fns/tz@1.2.0':
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
@@ -1149,15 +1106,6 @@ packages:
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@exodus/bytes@1.11.0':
resolution: {integrity: sha512-wO3vd8nsEHdumsXrjGO/v4p6irbg7hy9kvIeR6i2AwylZSk4HJdWgL0FNaVquW1+AweJcdvU1IEpuIWk/WaPnA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
'@noble/hashes': ^1.8.0 || ^2.0.0
peerDependenciesMeta:
'@noble/hashes':
optional: true
'@figspec/components@1.0.3':
resolution: {integrity: sha512-fBwHzJ4ouuOUJEi+yBZIrOy+0/fAjB3AeTcIHTT1PRxLz8P63xwC7R0EsIJXhScIcc+PljGmqbbVJCjLsnaGYA==}
@@ -2434,12 +2382,6 @@ packages:
'@types/vinyl@2.0.12':
resolution: {integrity: sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==}
'@types/whatwg-mimetype@3.0.2':
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@types/yargs-parser@21.0.3':
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
@@ -3055,9 +2997,6 @@ packages:
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
engines: {node: '>=12.0.0'}
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -3222,6 +3161,9 @@ packages:
'@chromatic-com/playwright':
optional: true
chrome-extension-toolkit@0.0.54:
resolution: {integrity: sha512-ux8v/PfWQIvO+EBbF+kDYq2z8Rnp5YZ7GwJxYX7R2a9owIEHJxiCUSJ82tOsiMQINF/31+t6QLG9equKNZUOlA==}
chrome-trace-event@1.0.4:
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
engines: {node: '>=6.0'}
@@ -3524,10 +3466,6 @@ packages:
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
cssstyle@5.3.7:
resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==}
engines: {node: '>=20'}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -3538,10 +3476,6 @@ packages:
resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==}
engines: {node: '>=12'}
data-urls@7.0.0:
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -3582,9 +3516,6 @@ packages:
supports-color:
optional: true
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
decode-named-character-reference@1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
@@ -3747,10 +3678,6 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
env-ci@11.1.0:
resolution: {integrity: sha512-Z8dnwSDbV1XYM9SBF2J0GcNVvmfmfh3a49qddGIROhBoVro6MZVTji15z/sJbQ2ko2ei8n988EU1wzoLU/tF+g==}
engines: {node: ^18.17 || >=20.6.1}
@@ -4467,10 +4394,6 @@ packages:
engines: {node: '>=0.4.7'}
hasBin: true
happy-dom@20.5.0:
resolution: {integrity: sha512-VQe+Q5CYiGOgcCERXhcfNsbnrN92FDEKciMH/x6LppU9dd0j4aTjCTlqONFOIMcAm/5JxS3+utowbXV1OoFr+g==}
engines: {node: '>=20.0.0'}
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
@@ -4548,10 +4471,6 @@ packages:
resolution: {integrity: sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==}
engines: {node: ^18.17.0 || >=20.5.0}
html-encoding-sniffer@6.0.0:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
@@ -4817,9 +4736,6 @@ packages:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -4986,15 +4902,6 @@ packages:
resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
engines: {node: '>=12.0.0'}
jsdom@28.0.0:
resolution: {integrity: sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -5205,10 +5112,6 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@11.2.5:
resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==}
engines: {node: 20 || >=22}
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -5818,9 +5721,6 @@ packages:
parse5@7.2.1:
resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==}
parse5@8.0.0:
resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
path-exists@3.0.0:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'}
@@ -6447,10 +6347,6 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
@@ -6796,9 +6692,6 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
synckit@0.9.2:
resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -6909,13 +6802,6 @@ packages:
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
engines: {node: '>=14.0.0'}
tldts-core@7.0.22:
resolution: {integrity: sha512-KgbTDC5wzlL6j/x6np6wCnDSMUq4kucHNm00KXPbfNzmllCmtmvtykJHfmgdHntwIeupW04y8s1N/43S1PkQDw==}
tldts@7.0.22:
resolution: {integrity: sha512-nqpKFC53CgopKPjT6Wfb6tpIcZXHcI6G37hesvikhx0EmUGPkZrujRyAjgnmp1SHNgpQfKVanZ+KfpANFt2Hxw==}
hasBin: true
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -6928,14 +6814,6 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tough-cookie@6.0.0:
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
engines: {node: '>=16'}
tr46@6.0.0:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'}
traverse@0.6.8:
resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==}
engines: {node: '>= 0.4'}
@@ -7070,10 +6948,6 @@ packages:
resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==}
engines: {node: '>=18.17'}
undici@7.21.0:
resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==}
engines: {node: '>=20.18.1'}
unicode-emoji-modifier-base@1.0.0:
resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==}
engines: {node: '>=4'}
@@ -7295,18 +7169,10 @@ packages:
typescript:
optional: true
w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
watchpack@2.4.4:
resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==}
engines: {node: '>=10.13.0'}
webidl-conversions@8.0.1:
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
engines: {node: '>=20'}
webpack-sources@3.3.3:
resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
engines: {node: '>=10.13.0'}
@@ -7328,22 +7194,10 @@ packages:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
whatwg-mimetype@5.0.0:
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
engines: {node: '>=20'}
whatwg-url@16.0.0:
resolution: {integrity: sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -7408,25 +7262,6 @@ packages:
utf-8-validate:
optional: true
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -7478,8 +7313,6 @@ packages:
snapshots:
'@acemir/cssom@0.9.31': {}
'@adobe/css-tools@4.4.2': {}
'@ampproject/remapping@2.3.0':
@@ -7501,24 +7334,6 @@ snapshots:
'@antfu/utils@8.1.1': {}
'@asamuzakjp/css-color@4.1.2':
dependencies:
'@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
lru-cache: 11.2.5
'@asamuzakjp/dom-selector@6.7.8':
dependencies:
'@asamuzakjp/nwsapi': 2.3.9
bidi-js: 1.0.3
css-tree: 3.1.0
is-potential-custom-element-name: 1.0.1
lru-cache: 11.2.5
'@asamuzakjp/nwsapi@2.3.9': {}
'@babel/code-frame@7.26.2':
dependencies:
'@babel/helper-validator-identifier': 7.25.9
@@ -7819,28 +7634,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@csstools/color-helpers@6.0.1': {}
'@csstools/css-calc@3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-color-parser@4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/color-helpers': 6.0.1
'@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-syntax-patches-for-csstree@1.0.26': {}
'@csstools/css-tokenizer@4.0.0': {}
'@date-fns/tz@1.2.0': {}
'@dnd-kit/accessibility@3.1.1(react@18.3.1)':
@@ -8123,8 +7916,6 @@ snapshots:
'@eslint/js@8.57.1': {}
'@exodus/bytes@1.11.0': {}
'@figspec/components@1.0.3':
dependencies:
lit: 2.8.0
@@ -9557,14 +9348,6 @@ snapshots:
'@types/expect': 1.20.4
'@types/node': 22.13.5
'@types/whatwg-mimetype@3.0.2':
optional: true
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.18.10
optional: true
'@types/yargs-parser@21.0.3': {}
'@types/yargs@17.0.33':
@@ -9937,7 +9720,7 @@ snapshots:
std-env: 3.8.0
test-exclude: 7.0.1
tinyrainbow: 1.2.0
vitest: 2.1.9(@types/node@22.13.5)(@vitest/ui@2.1.9)(happy-dom@20.5.0)(jsdom@28.0.0)(sass@1.85.1)(terser@5.44.0)
vitest: 2.1.9(@types/node@22.13.5)(@vitest/ui@2.1.9)(sass@1.85.1)(terser@5.44.0)
transitivePeerDependencies:
- supports-color
@@ -9999,7 +9782,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.12
tinyrainbow: 1.2.0
vitest: 2.1.9(@types/node@22.13.5)(@vitest/ui@2.1.9)(happy-dom@20.5.0)(jsdom@28.0.0)(sass@1.85.1)(terser@5.44.0)
vitest: 2.1.9(@types/node@22.13.5)(@vitest/ui@2.1.9)(sass@1.85.1)(terser@5.44.0)
'@vitest/utils@2.0.5':
dependencies:
@@ -10415,10 +10198,6 @@ snapshots:
dependencies:
open: 8.4.2
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
binary-extensions@2.3.0: {}
bl@5.1.0:
@@ -10595,6 +10374,11 @@ snapshots:
chromatic@11.26.0: {}
chrome-extension-toolkit@0.0.54:
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
chrome-trace-event@1.0.4: {}
clean-stack@2.2.0: {}
@@ -10937,26 +10721,12 @@ snapshots:
dependencies:
css-tree: 2.2.1
cssstyle@5.3.7:
dependencies:
'@asamuzakjp/css-color': 4.1.2
'@csstools/css-syntax-patches-for-csstree': 1.0.26
css-tree: 3.1.0
lru-cache: 11.2.5
csstype@3.1.3: {}
damerau-levenshtein@1.0.8: {}
dargs@8.1.0: {}
data-urls@7.0.0:
dependencies:
whatwg-mimetype: 5.0.0
whatwg-url: 16.0.0
transitivePeerDependencies:
- '@noble/hashes'
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.3
@@ -10989,8 +10759,6 @@ snapshots:
dependencies:
ms: 2.1.3
decimal.js@10.6.0: {}
decode-named-character-reference@1.0.2:
dependencies:
character-entities: 2.0.2
@@ -11143,8 +10911,6 @@ snapshots:
entities@4.5.0: {}
entities@6.0.1: {}
env-ci@11.1.0:
dependencies:
execa: 8.0.1
@@ -12219,19 +11985,6 @@ snapshots:
optionalDependencies:
uglify-js: 3.19.3
happy-dom@20.5.0:
dependencies:
'@types/node': 22.18.10
'@types/whatwg-mimetype': 3.0.2
'@types/ws': 8.18.1
entities: 4.5.0
whatwg-mimetype: 3.0.0
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
optional: true
has-bigints@1.1.0: {}
has-flag@3.0.0: {}
@@ -12319,12 +12072,6 @@ snapshots:
dependencies:
lru-cache: 10.4.3
html-encoding-sniffer@6.0.0:
dependencies:
'@exodus/bytes': 1.11.0
transitivePeerDependencies:
- '@noble/hashes'
html-escaper@2.0.2: {}
html-to-image@1.11.13: {}
@@ -12566,8 +12313,6 @@ snapshots:
is-plain-object@5.0.0: {}
is-potential-custom-element-name@1.0.1: {}
is-regex@1.2.1:
dependencies:
call-bound: 1.0.3
@@ -12728,32 +12473,6 @@ snapshots:
jsdoc-type-pratt-parser@4.1.0: {}
jsdom@28.0.0:
dependencies:
'@acemir/cssom': 0.9.31
'@asamuzakjp/dom-selector': 6.7.8
'@exodus/bytes': 1.11.0
cssstyle: 5.3.7
data-urls: 7.0.0
decimal.js: 10.6.0
html-encoding-sniffer: 6.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
parse5: 8.0.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 6.0.0
undici: 7.21.0
w3c-xmlserializer: 5.0.0
webidl-conversions: 8.0.1
whatwg-mimetype: 5.0.0
whatwg-url: 16.0.0
xml-name-validator: 5.0.0
transitivePeerDependencies:
- '@noble/hashes'
- supports-color
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
@@ -12942,8 +12661,6 @@ snapshots:
lru-cache@10.4.3: {}
lru-cache@11.2.5: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -13702,10 +13419,6 @@ snapshots:
dependencies:
entities: 4.5.0
parse5@8.0.0:
dependencies:
entities: 6.0.1
path-exists@3.0.0: {}
path-exists@4.0.0: {}
@@ -14391,10 +14104,6 @@ snapshots:
optionalDependencies:
'@parcel/watcher': 2.5.1
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
@@ -14818,8 +14527,6 @@ snapshots:
csso: 5.0.5
picocolors: 1.1.1
symbol-tree@3.2.4: {}
synckit@0.9.2:
dependencies:
'@pkgr/core': 0.1.1
@@ -14918,12 +14625,6 @@ snapshots:
tinyspy@3.0.2: {}
tldts-core@7.0.22: {}
tldts@7.0.22:
dependencies:
tldts-core: 7.0.22
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -14934,14 +14635,6 @@ snapshots:
totalist@3.0.1: {}
tough-cookie@6.0.0:
dependencies:
tldts: 7.0.22
tr46@6.0.0:
dependencies:
punycode: 2.3.1
traverse@0.6.8: {}
trim-lines@3.0.1: {}
@@ -15075,8 +14768,6 @@ snapshots:
undici@6.22.0: {}
undici@7.21.0: {}
unicode-emoji-modifier-base@1.0.0: {}
unicorn-magic@0.1.0: {}
@@ -15306,7 +14997,7 @@ snapshots:
sass: 1.85.1
terser: 5.44.0
vitest@2.1.9(@types/node@22.13.5)(@vitest/ui@2.1.9)(happy-dom@20.5.0)(jsdom@28.0.0)(sass@1.85.1)(terser@5.44.0):
vitest@2.1.9(@types/node@22.13.5)(@vitest/ui@2.1.9)(sass@1.85.1)(terser@5.44.0):
dependencies:
'@vitest/expect': 2.1.9
'@vitest/mocker': 2.1.9(vite@5.4.20(@types/node@22.13.5)(sass@1.85.1)(terser@5.44.0))
@@ -15331,8 +15022,6 @@ snapshots:
optionalDependencies:
'@types/node': 22.13.5
'@vitest/ui': 2.1.9(vitest@2.1.9)
happy-dom: 20.5.0
jsdom: 28.0.0
transitivePeerDependencies:
- less
- lightningcss
@@ -15360,17 +15049,11 @@ snapshots:
optionalDependencies:
typescript: 5.7.3
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
watchpack@2.4.4:
dependencies:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
webidl-conversions@8.0.1: {}
webpack-sources@3.3.3: {}
webpack-virtual-modules@0.6.2: {}
@@ -15409,21 +15092,8 @@ snapshots:
dependencies:
iconv-lite: 0.6.3
whatwg-mimetype@3.0.0:
optional: true
whatwg-mimetype@4.0.0: {}
whatwg-mimetype@5.0.0: {}
whatwg-url@16.0.0:
dependencies:
'@exodus/bytes': 1.11.0
tr46: 6.0.0
webidl-conversions: 8.0.1
transitivePeerDependencies:
- '@noble/hashes'
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
@@ -15507,13 +15177,6 @@ snapshots:
ws@8.18.1: {}
ws@8.19.0:
optional: true
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}
xtend@4.0.2: {}
y18n@5.0.8: {}

View File

@@ -1,8 +0,0 @@
# chrome-extension-toolkit
Repo: https://github.com/sghsri/chrome-extension-toolkit
Author: sghsri
License: MIT
Version: 0.0.96
This library has been inlined into UTRP due to the package being removed from npm.

View File

@@ -1,62 +0,0 @@
import React, { useState } from 'react';
const containerStyle: React.CSSProperties = {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
padding: '8px',
fontSize: '20px',
textAlign: 'center',
borderTopLeftRadius: '20px',
borderTopRightRadius: '20px',
zIndex: 999999999999,
cursor: 'pointer',
};
const closeButtonStyle: React.CSSProperties = {
position: 'absolute',
top: 0,
right: 0,
padding: '8px',
paddingRight: '20px',
fontSize: '20px',
cursor: 'pointer',
};
interface Props {
className?: string;
onClick?: () => void;
}
/**
* A component that displays a message onto a content script when the context extension is invalidated.
*/
export function ContextInvalidated(props: Props): JSX.Element | null {
const [isShowing, setIsShowing] = useState(true);
const hide = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
setIsShowing(false);
};
const reload = () => {
window.location.reload();
};
return isShowing ? (
<div
style={{
...containerStyle,
}}
id='extension-context-invalidated'
className={props.className}
onClick={props.onClick ?? reload}
>
Context Extension Context invalidated. Click to reload
<div style={closeButtonStyle} onClick={hide}>
</div>
</div>
) : null;
}

View File

@@ -1,2 +0,0 @@
export * from './ContextInvalidated';
export * from './onContextInvalidated';

View File

@@ -1,14 +0,0 @@
/**
* A content script can be invalidated if a chrome extension is reloaded / updated.
* This function is used to detect when the extension's context has been invalidated, and to call a callback.
* @param callback A callback to be called when the extension's context has been invalidated
*/
export function onContextInvalidated(callback: () => void) {
const interval = setInterval(() => {
// this means the current tab's context has been invalidated
if (!chrome.runtime.id) {
clearInterval(interval);
callback();
}
}, 1 * 1000);
}

View File

@@ -1,55 +0,0 @@
/**
* An extension of HTMLDivElement that represents a shadow root for use within an Extension Content Script.
*/
interface HTMLShadowDOMElement extends HTMLDivElement {
shadowRoot: ShadowRoot & {
INJECTION_POINT: HTMLDivElement;
};
/**
* Adds a style sheet to the shadow root.
* @param path the path to the style sheet relative to the extension's root directory. uses chrome.runtime.getURL to get the absolute path.
*/
addStyle(path: string): Promise<void>;
}
/**
* In extension content scripts, often times the parent site's styles will override the styles of the extension.
* To get around this, we create a shadow DOM and isolate the extension's html and styles in the shadow DOM.
* from the parent site's styles to prevent conflicts.
* @param id the id of the shadow root.
* @param options the optional options for the shadow root.
* @param isolate whether or not to isolate the extension's document flow from the parent site's document flow.
* @returns A Div that represents the shadow root with some additional methods added to it.
*/
export function createShadowDOM(id: string, options?: ShadowRootInit, isolate = false): HTMLShadowDOMElement {
const html = document.querySelector('html');
if (!html) {
throw new Error('Could not find html element');
}
const div = document.createElement('div') as HTMLShadowDOMElement;
div.id = id;
div.style.all = 'initial';
div.attachShadow({
mode: 'open',
...(options || {}),
});
const INJECTION_POINT = document.createElement('div');
INJECTION_POINT.id = 'INJECTION_POINT';
div.shadowRoot.appendChild(INJECTION_POINT);
div.shadowRoot.INJECTION_POINT = INJECTION_POINT;
div.addStyle = async (path: string) => {
const style = await fetch(chrome.runtime.getURL(path));
const styleNode = document.createElement('style');
const parsedStyle = await style.text();
styleNode.textContent = parsedStyle;
div.shadowRoot.appendChild(styleNode);
};
html.appendChild(div);
if (isolate) document.body.style.isolation = 'isolate';
return div as HTMLShadowDOMElement;
}

View File

@@ -1 +0,0 @@
export * from './createShadowRoot';

View File

@@ -1,70 +0,0 @@
/**
* Possible contexts in which a chrome extension can run.
*/
export enum ScriptType {
CONTENT_SCRIPT = 'content_script',
BACKGROUND_SCRIPT = 'background_script',
EXTENSION_POPUP = 'extension_popup',
EXTENSION_PAGE = 'extension_page',
}
/**
* Chrome extension code can run in different contexts.
* These different contexts have different capabilities and access to certain parts of the chrome extensions API.
* For example, the chrome.tabs api is not readily available in the content scripts.
* This function is used to identify the context in which the code is running.
* @returns The context in which the code is running, or null if the code is not running in a chrome extension.
*/
export default function getScriptType(): ScriptType | null {
if (!chrome?.runtime?.id) {
// we are not in a chrome extension
return null;
}
const manifest = chrome.runtime.getManifest();
if (globalThis.window === undefined) {
return ScriptType.BACKGROUND_SCRIPT;
}
if (window.location.href.startsWith(`chrome-extension://${chrome.runtime.id}`)) {
if (manifest.action?.default_popup && window.location.href.includes(manifest.action.default_popup)) {
return ScriptType.EXTENSION_POPUP;
}
return ScriptType.EXTENSION_PAGE;
}
return ScriptType.CONTENT_SCRIPT;
}
/**
* A helper function to check if the code is running in a content script.
* @returns true if the code is running in a content script, false otherwise.
*/
export function isContentScript(): boolean {
return getScriptType() === ScriptType.CONTENT_SCRIPT;
}
/**
* A helper function to check if the code is running in the background script.
* @returns true if the code is running in the background script, false otherwise.
*/
export function isBackgroundScript(): boolean {
return getScriptType() === ScriptType.BACKGROUND_SCRIPT;
}
/**
* A helper function to check if the code is running in the extension popup.
* @returns true if the code is running in the extension popup, false otherwise.
*/
export function isExtensionPopup(): boolean {
return getScriptType() === ScriptType.EXTENSION_POPUP;
}
/**
* A helper function to check if the code is running in an extension page (popup, options, etc.).
* @returns true if the code is running in an extension page (popup, options, etc.), false otherwise.
* @param pageName The name of the page to check for. ex: 'options.html'
*/
export function isExtensionPage(pageName?: string): boolean {
const isPage = getScriptType() === ScriptType.EXTENSION_PAGE;
return isPage && pageName ? window.location.href.includes(pageName) : isPage;
}

View File

@@ -1,6 +0,0 @@
export * from './context';
export * from './dom';
export * from './getScriptType';
export * from './messaging';
export * from './storage';
export * from './types';

View File

@@ -1,127 +0,0 @@
import getScriptType, { ScriptType } from '../getScriptType';
import type { IMessageListener, Message, MessageHandler, Serializable } from '../types';
import { MessageEndpoint } from '../types';
/**
* Options for configuring a message listener.
*/
export interface MessageListenerOptions {
/**
* A callback function that will be called when an error occurs.
* Useful if you want to log errors to a service like Sentry or Bugsnag.
* @param error The error that occurred.
*/
onError?: (error: Error) => void;
/**
* A flag indicating whether verbose logging should be enabled.
*/
verbose?: boolean;
}
/**
* An object that can be used to listen for and handle messages coming from another extension context.
*/
export class MessageListener<M> implements IMessageListener<M> {
private handlers: MessageHandler<M>;
private scriptType: ScriptType;
private myEndpoint: MessageEndpoint;
private listeningFor: MessageEndpoint;
private onError?: (error: Error) => void;
private isVerbose: boolean = false;
/**
* An object that can be used to listen for and handle messages coming from another extension context.
* @param handlers the message handlers for the messages that this listener will handle. When a message is received, the corresponding message handler is called.
*/
constructor(handlers: MessageHandler<M>) {
this.handlers = handlers;
// we want to know what type of script we are running in so we can determine what endpoint we are (background or foreground)
const scriptType = getScriptType();
if (!scriptType) {
throw new Error('[crx-kit]: Unable to determine extension script type.');
}
this.scriptType = scriptType;
if (this.scriptType === ScriptType.BACKGROUND_SCRIPT) {
this.myEndpoint = MessageEndpoint.BACKGROUND;
this.listeningFor = MessageEndpoint.FOREGROUND;
} else {
this.myEndpoint = MessageEndpoint.FOREGROUND;
this.listeningFor = MessageEndpoint.BACKGROUND;
}
}
private handleMessage = (
message: Message<M>,
sender: chrome.runtime.MessageSender,
sendResponse: (response: any) => void
): boolean => {
if (message.to !== this.myEndpoint && message.from !== this.listeningFor) {
// this message is not for my current context, so ignore it
return true;
}
const messageName = message.name as string;
// @ts-expect-error
const handler = this.handlers[messageName];
if (!handler) {
// this message is for my current context, but I don't have a handler for it, so ignore it
return true;
}
try {
if (this.isVerbose) {
console.log(`[crx-kit]: message received: ${messageName}`, {
name: messageName,
data: message.data,
sender,
});
}
// this message is for my current context, and I have a handler for it, so handle it
handler({
data: message.data as Serializable<typeof message.data>,
sendResponse,
sender,
});
} catch (error) {
console.error(`[crx-kit]: Error handling message ${messageName}`, {
name: messageName,
error,
message,
sender,
});
if (this.onError) {
// @ts-expect-error
this.onError(error);
}
}
return true;
};
/**
* Listens for messages from the specified source.
* @param options - The options for the message listener.
*/
public listen(options: MessageListenerOptions = { verbose: false }) {
this.isVerbose = options.verbose ?? false;
this.onError = options.onError;
console.log(`[crx-kit]: ${this.toString()} listening for messages from ${this.listeningFor}`);
chrome.runtime.onMessage.addListener(this.handleMessage);
}
/**
* Stops listening for messages
*/
public unlisten() {
console.log(`[crx-kit]: ${this.toString()} no longer listening for messages from ${this.listeningFor}`);
chrome.runtime.onMessage.removeListener(this.handleMessage);
}
private toString() {
return `MessageListener(${this.myEndpoint})`;
}
}

View File

@@ -1,94 +0,0 @@
import type { Message, MessageData, MessageResponse } from '../types';
import { MessageEndpoint } from '../types';
/**
* An object that can be used to send messages to the background script.
*/ export type BackgroundMessenger<M> = {
[K in keyof M]: MessageData<M, K> extends undefined
? () => Promise<MessageResponse<M, K>>
: (data: MessageData<M, K>) => Promise<MessageResponse<M, K>>;
};
/**
* Where the foreground message is being sent to specifically (which tab or frame)
*/
type ForegroundMessageOptions =
| {
tabId: number;
frameId?: number;
}
| {
tabId: 'ALL' | 'ACTIVE_TAB';
};
/**
* an object that can be used to send messages to the foreground (tabs OR extension pages (popup, options, etc.))
*/
export type ForegroundMessenger<M> = {
[K in keyof M]: MessageData<M, K> extends undefined
? (options: ForegroundMessageOptions) => Promise<MessageResponse<M, K>>
: (data: MessageData<M, K>, options: ForegroundMessageOptions) => Promise<MessageResponse<M, K>>;
};
/**
* A wrapper for chrome extension messaging with a type-safe API.
* @type To which context the messages are sent.
* @returns A proxy object that can be used to send messages to the foreground (tabs or extension pages (popup, options, etc.))
*/
export function createMessenger<M>(destination: 'foreground'): ForegroundMessenger<M>;
/**
* A wrapper for chrome extension messaging with a type-safe API.
* @param type To which context the messages are sent.
* @returns A proxy object that can be used to send messages to the background script.
*/
export function createMessenger<M>(destination: 'background'): BackgroundMessenger<M>;
/**
* A wrapper for chrome extension messaging with a type-safe API.
* @param destination To which context the messages are sent.
* @returns A proxy object that can be used to send messages to the background script.
*/
export function createMessenger<M>(destination: 'background' | 'foreground') {
let to: MessageEndpoint = MessageEndpoint.BACKGROUND;
let from: MessageEndpoint = MessageEndpoint.FOREGROUND;
if (destination === 'foreground') {
to = MessageEndpoint.FOREGROUND;
from = MessageEndpoint.BACKGROUND;
}
const sender = new Proxy({} as any, {
get(target, prop) {
const name = prop as keyof M;
return async (data: MessageData<M, any>, options?: ForegroundMessageOptions) => {
const message: Message<M> = {
name,
data,
from,
to,
};
if (to === MessageEndpoint.FOREGROUND && options) {
// for messages sent to the tabs, we want to send to the tabs using chrome.tabs.sendMessage,
const { tabId } = options;
if (typeof tabId === 'number') {
return chrome.tabs.sendMessage(tabId, message, { frameId: options.frameId });
}
if (tabId === 'ACTIVE_TAB') {
const tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
if (tab && tab.id) {
return chrome.tabs.sendMessage(tab.id, message);
}
}
if (tabId === 'ALL') {
const tabs = (await chrome.tabs.query({})).filter(tab => tab.id !== undefined && tab.url);
return Promise.any([
...tabs.map(tab => chrome.tabs.sendMessage(tab.id!, message)),
chrome.runtime.sendMessage(message),
]);
}
}
return chrome.runtime.sendMessage(message);
};
},
});
return sender;
}

View File

@@ -1,30 +0,0 @@
import { useEffect } from 'react';
import type { Message, MessageData } from '../types';
/**
* A helper function to create a hook that can listen for messages coming through chrome.runtime.onMessage
* with e2e type safety
* @returns a hook that can be used to listen for messages from the background script.
*/
export function createUseMessage<M>() {
return function useMessage<N extends keyof M, D extends MessageData<M, N>>(
name: N,
callback: (data: D) => void
): void {
useEffect(() => {
const onMessage = (message: Message<M>) => {
if (message.name === name) {
callback(message.data);
}
return true;
};
chrome.runtime.onMessage.addListener(onMessage);
return () => {
chrome.runtime.onMessage.removeListener(onMessage);
};
}, [name, callback]);
};
}

View File

@@ -1,3 +0,0 @@
export * from './createMessenger';
export * from './createUseMessage';
export * from './MessageListener';

View File

@@ -1,15 +0,0 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
CI?: string;
EXTENSION_STORAGE_PASSWORD?: string;
}
}
type Environment = typeof process.env.NODE_ENV;
}
// If this file has no import/export statements (i.e. is a script)
// convert it into a module by adding an empty export statement.
export {};

View File

@@ -1,214 +0,0 @@
/* eslint-disable max-classes-per-file */
const { crypto } = globalThis;
/** the number of times the key_algo will be run on the password */
const ITERATIONS = 470_000;
/** the algorithm used to derive the password key from the password */
const KEY_ALGO = 'PBKDF2';
/** the algorithm used to derive the cipher key from the password key */
const HASH_ALGO = 'SHA-256';
/** the algorithm used to encrypt the data */
const CIPHER_MODE = 'AES-GCM';
/** the size of the cipher key */
const CIPHER_SIZE = 256;
/**
* A class that provides encryption and decryption methods for use when storing data in the Stores provided by this library.
*/
export class Security {
private encoder = new TextEncoder();
private decoder = new TextDecoder();
private cachedPasswordKey?: CryptoKey;
public static MISSING_PASSWORD_ERROR_MESSAGE =
'You must set the EXTENSION_STORAGE_PASSWORD environment variable to use encrypted storage.';
/**
* @returns the password key, either from the cache or by generating a new one from the password environment variable
*/
private async getPasswordKey(): Promise<CryptoKey> {
if (this.cachedPasswordKey) return this.cachedPasswordKey;
if (!process.env.EXTENSION_STORAGE_PASSWORD) {
throw new Error(Security.MISSING_PASSWORD_ERROR_MESSAGE);
}
const passwordBuffer = this.encoder.encode(process.env.EXTENSION_STORAGE_PASSWORD);
this.cachedPasswordKey = await crypto.subtle.importKey(
'raw',
passwordBuffer,
{ name: KEY_ALGO },
false, // Not exportable
['deriveKey']
);
return this.cachedPasswordKey;
}
/**
* Derives a cipher key from the password key and salt
* @param salt the salt to use when deriving the cipher key
* @param passKey the password key to use when deriving the cipher key
* @param KeyUsage the key usage for the cipher key (encrypt or decrypt)
* @returns the cipher key which can be used to encrypt or decrypt data
*/
private async deriveCipherKey(salt: Uint8Array, passKey: CryptoKey, KeyUsage: KeyUsage[]): Promise<CryptoKey> {
return crypto.subtle.deriveKey(
{
name: KEY_ALGO,
hash: HASH_ALGO,
salt,
iterations: ITERATIONS,
},
passKey,
{
name: CIPHER_MODE,
length: CIPHER_SIZE,
},
false,
KeyUsage
);
}
/**
* @returns a random salt buffer for use in encryption
*/
private deriveSalt(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(BoxBuffer.SALT_SIZE));
}
/**
* @returns a random IV buffer for use in encryption
*/
private deriveIv(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(BoxBuffer.IV_SIZE));
}
/**
* Decrypts a value that has been encrypted using the encrypt method
* @param value the encrypted value to decrypt
* @returns the decrypted value
*/
async decrypt(value: any) {
if (!value) return value;
if (!isString(value)) return value;
const passKey = await this.getPasswordKey();
// read in the string into a boxBuffer, and separate the salt, iv and encrypted data from it
const boxBuffer = BoxBuffer.fromBase64String(value);
const salt = boxBuffer.getSalt();
const iv = boxBuffer.getIv();
const encryptedData = boxBuffer.getEncryptedData();
// generate the decryption key
const decryptionKey = await this.deriveCipherKey(salt, passKey, ['decrypt']);
// decrypt the data using the decryption key
const decryptedDataBuffer = await crypto.subtle.decrypt(
{
name: CIPHER_MODE,
iv,
},
decryptionKey,
encryptedData
);
// parse the decrypted data into a JSON object and return it
return decryptedDataBuffer.byteLength === 0 ? undefined : JSON.parse(this.decoder.decode(decryptedDataBuffer));
}
/**
* Encrypts a value using the password key derived from the EXTENSION_STORAGE_PASSWORD environment variable
* @param value the value to encrypt
* @returns the encrypted value as a base64 string
*/
async encrypt(value: any): Promise<string> {
let valueString = JSON.stringify(value);
const passKey = await this.getPasswordKey();
const salt = this.deriveSalt();
const iv = this.deriveIv();
const encryptionKey = await this.deriveCipherKey(salt, passKey, ['encrypt']);
const encryptedData = new Uint8Array(
await crypto.subtle.encrypt(
{
name: CIPHER_MODE,
iv,
},
encryptionKey,
this.encoder.encode(valueString)
)
);
// create a boxBuffer to store the salt, iv and encrypted data together in a single buffer
const bufferSize = BoxBuffer.PREFIX_SIZE + encryptedData.byteLength;
const boxBuffer = new BoxBuffer(new Uint8Array(bufferSize));
boxBuffer.setSalt(salt);
boxBuffer.setIv(iv);
boxBuffer.setEncryptedData(encryptedData);
// return the boxBuffer as a base64 string (to make it easier to store)
return boxBuffer.toBase64String();
}
}
/**
* A class representation of a buffer box in memory of the form:
* [salt][iv][encrypted data]
*/
class BoxBuffer {
private buffer: Uint8Array;
static SALT_SIZE = 16;
static IV_SIZE = 32;
static get PREFIX_SIZE() {
return BoxBuffer.SALT_SIZE + BoxBuffer.IV_SIZE;
}
constructor(buffer: Uint8Array) {
this.buffer = buffer;
}
setSalt(salt: Uint8Array) {
this.buffer.set(salt, 0);
}
setIv(iv: Uint8Array) {
this.buffer.set(iv, BoxBuffer.SALT_SIZE);
}
setEncryptedData(encryptedData: Uint8Array) {
this.buffer.set(encryptedData, BoxBuffer.PREFIX_SIZE);
}
getSalt(): Uint8Array {
return this.buffer.slice(0, BoxBuffer.SALT_SIZE);
}
getIv(): Uint8Array {
return this.buffer.slice(BoxBuffer.SALT_SIZE, BoxBuffer.PREFIX_SIZE);
}
getEncryptedData(): Uint8Array {
return this.buffer.slice(BoxBuffer.PREFIX_SIZE);
}
toBase64String() {
return globalThis.btoa(String.fromCharCode.apply(null, this.buffer as any));
}
static fromBase64String(base64String: string): BoxBuffer {
const rawBuffer = Uint8Array.from(globalThis.atob(base64String), c => c.charCodeAt(0));
return new BoxBuffer(rawBuffer);
}
}
function isString(value: any): value is string {
return typeof value === 'string' || value instanceof String;
}

View File

@@ -1,447 +0,0 @@
/* eslint-disable no-await-in-loop */
import { useEffect, useState } from 'react';
import type { Serializable } from '../types';
import { Security } from './Security';
/** A utility type that forces you to declare all the values specified in the type interface for a module. */
export type StoreDefaults<T> = {
[P in keyof Required<T>]: Pick<T, P> extends Required<Pick<T, P>> ? T[P] : T[P] | undefined;
};
/**
* Represents a change in data within the store.
*/
export type DataChange<T> = {
key: string;
/**
* The old value of the data. This will be undefined if the data was just initialized.
*/
oldValue?: Serializable<T>;
/**
* The new value of the data.
*/
newValue: Serializable<T>;
};
/**
* A function that is called when the data in the store changes.
*/
export type OnChangedFunction<T> = (changes: DataChange<T>) => void;
/**
* A virtual wrapper around the chrome.storage API that allows you to segment and compartmentalize your data.
* The data is all stored at the top level of the storage area, so you should namespace your keys to avoid collisions.
*/
export type Store<T = {}> = {
/**
* A unique identifier for the store. This will be prepended to all keys in the store to avoid collisions.
*/
readonly storeId: string;
/**
* The options that were passed to the createStore function
*/
readonly options: StoreOptions;
/**
* The default values for the store. These were passed to the createStore function and will be used to initialize the store if the key is not already set.
*/
readonly defaults: StoreDefaults<T>;
/**
* Initializes the store by setting any keys that are not already set to their default values. This will be called automatically when you first access a getter or setter.
*/
initialize(): Promise<void>;
/**
* Gets the value of the specified key from the store.
* @param key the key to get the value of
* @returns a promise that resolves to the value of the specified key (wrapped in a Serialized type)
*/
get<K extends keyof T>(key: K): Promise<Serializable<T[K]>>;
/**
* Sets the value of the specified key in the store.
* @param key the key to set the value of
* @param value the value to set the key to
*/
set<K extends keyof T>(key: K, value: Serializable<T[K]>): Promise<void>;
/**
* Sets the store with the values in the object passed in.
* @param values an object containing the keys and values to set in the store
*/
set(values: Partial<Serializable<T>>): Promise<void>;
/**
* Removes a specific key from the store.
* @param key the key to remove from the store
*/
remove<K extends keyof T>(key: K): Promise<void>;
/**
* Returns a promise that resolves to the entire contents of the store.
*/
all(): Promise<Serializable<T>>;
/**
* Returns an array of all the keys in the store.
*/
keys(): (keyof T & string)[];
/**
* A react hook that allows you to get and set the value of the specified key in the store from a functional component.
* @param key the key to get the value of or null to get the entire store
* @returns a tuple containing the value of the specified key, and a function to set the value
*/
use<K extends keyof T | null>(
key: K
): [
K extends keyof T ? Serializable<T[K]> : Serializable<T>,
(value: K extends keyof T ? Serializable<T[K]> : Partial<Serializable<T>>) => Promise<void>,
];
/**
* A react hook that allows you to get and set the value of the specified key in the store from a functional component.
* @param key the key to get the value of or null to get the entire store
* @param defaultValue the default value to use if the key is not already set
*/
use<K extends keyof T | null>(
key: K,
defaultValue: K extends keyof T ? Serializable<T[K]> : Serializable<T>
): [
K extends keyof T ? Serializable<T[K]> : Serializable<T>,
(value: K extends keyof T ? Serializable<T[K]> : Partial<Serializable<T>>) => Promise<void>,
];
/**
* Subscribes to changes in the specified key in the store, and calls the specified function when the key changes.
* @param key the key to subscribe to
* @param callback the function to call when the key changes
*/
// @ts-expect-error
subscribe<K extends keyof T>(key: K, callback: OnChangedFunction<T[K]>): (changes, area) => void;
// @ts-expect-error
subscribe<K extends keyof T>(key: K[], callback: OnChangedFunction<T[K]>): (changes, area) => void;
/**
* Removes a subscription that was added with the subscribe function.
* @param sub the subscription function that was added
*/
// @ts-expect-error
unsubscribe(sub: (changes, area) => void): void;
};
/**
* Options that modify the behavior of the store
*/
type StoreOptions = {
/**
* Whether or not to encrypt the data before storing it, and decrypt it when retrieving it. Defaults to false.
*/
isEncrypted?: boolean;
};
const security = new Security();
/**
* A function that creates a virtual storage bucket within the chrome.storage API.
*
* @param defaults the default values for the store (these will be used to initialize the store if the key is not already set, and will be used as the type for the getters and setters)
* @param area the storage area to use. Defaults to 'local'
* @returns an object which contains getters/setters for the keys in the defaults object, as well as an initialize function and an onChanged functions
*/
function createStore<T>(
storeId: string,
defaults: StoreDefaults<T>,
area: 'sync' | 'local' | 'session' | 'managed',
options?: StoreOptions
): Store<T> {
const keys = Object.keys(defaults) as string[];
const actualKeys = keys.map(key => `${storeId}:${key}`);
let isEncrypted = options?.isEncrypted || false;
if (isEncrypted && !process.env.EXTENSION_STORAGE_PASSWORD) {
throw new Error(Security.MISSING_PASSWORD_ERROR_MESSAGE);
}
const store = {
defaults,
storeId,
options,
} as Store<T>;
let hasInitialized = false;
store.initialize = async () => {
const data = await chrome.storage[area].get(actualKeys);
const missingKeys = actualKeys.filter(key => data[key] === undefined);
if (missingKeys.length) {
const defaultsToSet = {};
for (const key of missingKeys) {
// @ts-expect-error
const value = defaults[key.replace(`${storeId}:`, '')];
// @ts-expect-error
defaultsToSet[key] = isEncrypted ? await security.encrypt(value) : value;
}
await chrome.storage[area].set(defaultsToSet);
}
hasInitialized = true;
};
store.get = async (key: any) => {
if (!hasInitialized) {
await store.initialize();
}
const actualKey = `${storeId}:${key}`;
const value = (await chrome.storage[area].get(actualKey))[actualKey];
return isEncrypted ? await security.decrypt(value) : value;
};
store.set = async (key: any, value?: any) => {
if (!hasInitialized) {
await store.initialize();
}
// Handle the case where key is an object
if (typeof key === 'object' && value === undefined) {
const entriesToRemove: string[] = [];
const entriesToSet = {};
for (const [k, v] of Object.entries(key)) {
const actualKey = `${storeId}:${k}`;
if (v === undefined) {
// Prepare to remove this key
entriesToRemove.push(actualKey);
} else {
// @ts-expect-error
entriesToSet[actualKey] = isEncrypted ? await security.encrypt(v) : v;
}
}
// Remove keys with undefined values
if (entriesToRemove.length > 0) {
await chrome.storage[area].remove(entriesToRemove);
}
// Set keys with defined values
if (Object.keys(entriesToSet).length > 0) {
await chrome.storage[area].set(entriesToSet);
}
return;
}
// now we know key is a string, so lets either set or remove it directly
const actualKey = `${storeId}:${key}`;
if (value === undefined) {
// Remove if value is explicitly undefined
return await chrome.storage[area].remove(actualKey);
}
// Set the value, applying encryption if necessary
await chrome.storage[area].set({
[actualKey]: isEncrypted ? await security.encrypt(value) : value,
});
};
store.remove = async (key: any) => {
if (!hasInitialized) {
await store.initialize();
}
const actualKey = `${storeId}:${key}`;
await chrome.storage[area].remove(actualKey);
};
store.all = async () => {
if (!hasInitialized) {
await store.initialize();
}
const fullStore = await chrome.storage[area].get(actualKeys);
if (isEncrypted) {
await Promise.all(
keys.map(async key => {
const actualKey = `${storeId}:${key}`;
fullStore[key] = await security.decrypt(fullStore[actualKey]);
})
);
}
// now we need to remove the storeId from the keys
Object.keys(fullStore).forEach(actualKey => {
const newKey = actualKey.replace(`${storeId}:`, '');
fullStore[newKey] = fullStore[actualKey];
delete fullStore[actualKey];
});
return fullStore as Serializable<T>;
};
store.keys = () => keys as (keyof T & string)[];
store.subscribe = (key: string | string[], callback) => {
// @ts-expect-error
const sub = async (changes, areaName) => {
const keys = Array.isArray(key) ? key : [key];
const actualKeys = keys.map(k => `${storeId}:${k}`);
if (areaName !== area) return;
// make sure that there are keys is in the changes object
const subKeys = Object.keys(changes).filter(k => actualKeys.includes(k));
if (!subKeys.length) return;
subKeys.forEach(async actualKey => {
const key = actualKey.replace(`${storeId}:`, '');
const [oldValue, newValue] = await Promise.all([
isEncrypted ? security.decrypt(changes[actualKey].oldValue) : changes[actualKey].oldValue,
isEncrypted ? security.decrypt(changes[actualKey].newValue) : changes[actualKey].newValue,
]);
callback({
key,
oldValue,
newValue,
});
});
};
chrome.storage.onChanged.addListener(sub);
return sub;
};
store.unsubscribe = sub => {
chrome.storage.onChanged.removeListener(sub);
};
// @ts-ignore
store.use = (key: keyof T | null, defaultValue?: key extends null ? T : T[typeof key]) => {
const initialValue: any = (() => {
// an explicit default value was passed, use it
if (arguments.length === 2) {
return defaultValue;
}
// a key was passed, but no default value was passed, use the default value from the defaults object
if (key === null) {
return defaults;
}
// no key was passed, use the default value from the defaults object
return defaults[key];
})();
const [value, setValue] = useState(initialValue);
const onChange = ({ key: k, newValue }: DataChange<T>) => {
if (key === null) {
// @ts-expect-error
setValue(prev => ({ ...prev, [k]: newValue }) as any);
} else {
setValue(newValue as any);
}
};
useEffect(() => {
if (key === null) {
store
.all()
.then(setValue)
.then(() => {
store.subscribe(store.keys(), onChange as any);
});
} else {
store
.get(key)
.then(setValue)
.then(() => {
store.subscribe(key, onChange as any);
});
}
return () => {
store.unsubscribe(onChange as any);
};
}, []);
// @ts-expect-error
const set = async newValue => {
if (key === null) {
await store.set(newValue as any);
} else {
await store.set(key, newValue as any);
}
};
return [value, set] as any;
};
return store;
}
/**
* A function that creates a virtual storage bucket within the chrome.storage.local API.
* This store will persist across browser sessions and be stored on the user's computer.
*
* @param storeId A unique key to use for this store. This will be prepended to all keys in the store to avoid collisions. ex: 'my-store' -> 'my-store:myKey'
* @param defaults the default values for the store (these will be used to initialize the store if the key is not already set, and will be used as the type for the getters and setters)
* @param computed an optional function that allows you to override the generated getters and setters with your own. Provides a reference to the store itself so you can access this store's getters and setters.
* @param area the storage area to use. Defaults to 'local'
* @returns an object which contains getters/setters for the keys in the defaults object, as well as an initialize function and an onChanged functions
*/
export function createLocalStore<T>(storeId: string, defaults: StoreDefaults<T>, options?: StoreOptions): Store<T> {
return createStore(storeId, defaults, 'local', options);
}
/**
* A function that creates a virtual storage bucket within the chrome.storage.sync API.
* This store will persist across browser sessions and be stored on the user's Google account (if they are logged in).
* This means that the data will be synced across all of the user's devices.
*
* @param storeId A unique key to use for this store. This will be prepended to all keys in the store to avoid collisions. ex: 'my-store' -> 'my-store:myKey'
* @param defaults the default values for the store (these will be used to initialize the store if the key is not already set, and will be used as the type for the getters and setters)
* @param options options that modify the behavior of the store
* @returns an object which contains getters/setters for the keys in the defaults object, as well as an initialize function and an onChanged functions
*/
export function createSyncStore<T>(storeId: string, defaults: StoreDefaults<T>, options?: StoreOptions): Store<T> {
return createStore(storeId, defaults, 'sync', options);
}
/**
* A function that creates a virtual storage bucket within the chrome.storage.managed API.
* This store will persist across browser sessions and managed by the administrator of the user's computer.
*
* @param storeId A unique key to use for this store. This will be prepended to all keys in the store to avoid collisions. ex: 'my-store' -> 'my-store:myKey'
* @param defaults the default values for the store (these will be used to initialize the store if the key is not already set, and will be used as the type for the getters and setters)
* @param options options that modify the behavior of the store
* @returns an object which contains getters/setters for the keys in the defaults object, as well as an initialize function and an onChanged functions
* @see https://developer.chrome.com/docs/extensions/reference/storage/#type-ManagedStorageArea
*
*/
export function createManagedStore<T>(storeId: string, defaults: StoreDefaults<T>, options?: StoreOptions): Store<T> {
return createStore(storeId, defaults, 'managed', options);
}
/**
* A function that creates a virtual storage bucket within the chrome.storage.session API.
* This store will NOT persist across browser sessions and will be stored in memory. This will reset when the browser is closed.
*
* @param storeId A unique key to use for this store. This will be prepended to all keys in the store to avoid collisions. ex: 'my-store' -> 'my-store:myKey'
* @param defaults the default values for the store (these will be used to initialize the store if the key is not already set, and will be used as the type for the getters and setters)
* @param options options that modify the behavior of the store
* @returns an object which contains getters/setters for the keys in the defaults object, as well as an initialize function and an onChanged functions
*/
export function createSessionStore<T>(storeId: string, defaults: StoreDefaults<T>, options?: StoreOptions): Store<T> {
return createStore(storeId, defaults, 'session', options);
}
// interface MyStore {
// name: string;
// age: number;
// isCool?: boolean;
// }
// const store = createLocalStore<MyStore>('my-store', {
// age: 0,
// isCool: false,
// name: 'John Doe',
// });

View File

@@ -1,2 +0,0 @@
export * from './createStore';
export * from './Security';

View File

@@ -1,70 +0,0 @@
import type { Serializable } from './Serialization';
/**
* MessageDefinition is a record of message names and their data types.
* The data type is the type of the first argument of the message handler.
* The return type of the message handler is the resolved type of the promise returned by the message sender.
*/
export type MessageDefinition<T extends Record<string, (data: Record<string, any> | undefined) => any>> = T;
/**
* A helper type to extract the data type of a message handler.
*/
export type MessageData<M, K extends keyof M> = Serializable<
Parameters<M[K] extends (...args: any) => any ? M[K] : never>[0]
>;
/**
* A helper type to extract the resolved type of a message handler.
*/
export type MessageResponse<M, K extends keyof M> = Serializable<
ReturnType<M[K] extends (...args: any) => any ? M[K] : never>
>;
/**
* The internal object representing a message sent between chrome extension contexts.
*/
export type Message<M> = {
name: keyof M;
data: MessageData<M, keyof M>;
from: MessageEndpoint;
to: MessageEndpoint;
};
/**
* Represents the extension context in which a message is either sent or received.
* The context is either the background script/service worker or a tab.
*/
export enum MessageEndpoint {
/** The background script or service worker. */
BACKGROUND = 'BACKGROUND',
/** A foreground or extension page (popup, options, etc) */
FOREGROUND = 'FOREGROUND',
}
/**
* An object that implements the message handlers for each of the messages in the message definition.
*/
export type MessageHandler<M> = {
[K in keyof M]: (context: {
/** The data sent with the message. */
data: MessageData<M, K>;
/** The tab or page or background service worker that sent the message. */
sender: chrome.runtime.MessageSender;
/** A function that can be used to send a response asynchronously to the sender. */
sendResponse: (response: MessageResponse<M, K>) => void;
}) => Promise<void> | void;
};
/**
* An object that can be used to handle messages coming from another extension context.
*/
export interface IMessageListener<M> {
/**
* Starts listening for messages. When a message is received, the corresponding message handler is called.
*/
listen: (handler: MessageHandler<M>) => void;
/**
* Stops listening for messages.
*/
unlisten: () => void;
}

View File

@@ -1,69 +0,0 @@
type Primitive = string | number | boolean | null | undefined;
/**
* A type that represents a serialized object. This is a recursive type that will serialize all properties of an object, except for functions which are ignored (and thus not serialized)
*/
export type Serialized<T> = {
[K in keyof T as T[K] extends Function ? never : K]: Serializable<T[K]>;
};
/**
* Represents a type that can be serialized to JSON. This could either be a primitive type, an array of serializable types, or a complex object
* Recursively serializes an object to a JSON-compatible object.
*/
export type Serializable<T> = T extends Primitive
? T
: T extends Array<infer U>
? Array<Serializable<U>>
: T extends object
? Serialized<T>
: T;
/**
* Serializes a value to a JSON-compatible object (i.e. a Serializable<T>)
* @returns A JSON-compatible object that represents the value passed in with all functions removed and all objects recursively serialized
*/
export function serialize<T>(value: T): Serializable<T> {
return JSON.parse(JSON.stringify(value)) as Serializable<T>;
}
// THIS IS FOR TESTINWG THE TYPING
// type Test2 = {
// test: string;
// openNewTab: (url: string) => void;
// url?: URL;
// };
// type Test1 = {
// openNewTab: (url: string) => void;
// count: number;
// url: URL;
// urls?: URL[];
// foo: Test2;
// bar?: Test2;
// };
// let x: Serialized<Test1>;
// // ^?
// type SerializedData = Serializable<Data>;
// let data: SerializedData = {} as any;
// data.url = undefined;
// data = {
// a: 'test',
// b: 1,
// c: true,
// d: {
// e: 'test',
// t: undefined,
// },
// urls: [],
// };
// data.d = {
// e: 'test',
// };

View File

@@ -1,47 +0,0 @@
type Primitive = string | number | boolean | null | undefined;
type Serialized<T> = {
[K in keyof T as T[K] extends Function ? never : K]: Serializable<T[K]>;
};
type Serializable<T> = T extends Primitive
? T
: T extends Array<infer U>
? Array<Serializable<U>>
: T extends object
? Serialized<T>
: T;
type Data = {
a: string;
b: number;
c: boolean;
url?: URL;
d: {
e: string;
t?: URL;
};
get: () => void;
urls: URL[];
};
type SerializedData = Serializable<Data>;
let x: SerializedData = {} as any;
x.url = undefined;
x = {
a: 'test',
b: 1,
c: true,
d: {
e: 'test',
t: undefined,
},
urls: [],
};
x.d = {
e: 'test',
};

View File

@@ -1,6 +0,0 @@
export * from './Messaging';
export * from './Serialization';
export type AddParameters<TFunction extends (...args: any) => any, TParameters extends [...args: any]> = (
...args: [...Parameters<TFunction>, ...TParameters]
) => ReturnType<TFunction>;

View File

@@ -1,48 +0,0 @@
const LOG_BACKGROUND_COLOR = '#2196F3';
const LOG_TEXT_COLOR = '#fff';
const SUCCESS_BACKGROUND_COLOR = '#4CAF50';
const SUCCESS_TEXT_COLOR = '#fff';
const ERROR_BACKGROUND_COLOR = '#F44336';
const ERROR_TEXT_COLOR = '#fff';
const WARNING_BACKGROUND_COLOR = '#FFC107';
const WARNING_TEXT_COLOR = '#fff';
/**
* Utility class for logging messages with different styles.
*/
export class Console {
static log(...args: any[]) {
console.log(
`%c ${args[0]} `,
`background: ${LOG_BACKGROUND_COLOR}; color: ${LOG_TEXT_COLOR}`,
...args.slice(1)
);
}
static success(...args: any[]) {
console.log(
`%c ${args[0]} `,
`background: ${SUCCESS_BACKGROUND_COLOR}; color: ${SUCCESS_TEXT_COLOR}`,
...args.slice(1)
);
}
static error(...args: any[]) {
console.error(
`%c ${args[0]} `,
`background: ${ERROR_BACKGROUND_COLOR}; color: ${ERROR_TEXT_COLOR}`,
...args.slice(1)
);
}
static warn(...args: any[]) {
console.warn(
`%c ${args[0]} `,
`background: ${WARNING_BACKGROUND_COLOR}; color: ${WARNING_TEXT_COLOR}`,
...args.slice(1)
);
}
}

View File

@@ -1,8 +0,0 @@
/**
* Capitalizes the first letter of a string
* @param str the string to capitalize
* @returns the capitalized string
*/
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}

View File

@@ -1,7 +1,7 @@
import { MessageListener } from '@chrome-extension-toolkit';
import type { BACKGROUND_MESSAGES } from '@shared/messages';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import updateBadgeText from '@shared/util/updateBadgeText';
import { MessageListener } from 'chrome-extension-toolkit';
import onInstall from './events/onInstall';
import onServiceWorkerAlive from './events/onServiceWorkerAlive';
@@ -58,13 +58,13 @@ const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
messageListener.listen();
UserScheduleStore.subscribe('schedules', async schedules => {
UserScheduleStore.listen('schedules', async schedules => {
const index = await UserScheduleStore.get('activeIndex');
const numCourses = schedules.newValue[index]?.courses?.length;
updateBadgeText(numCourses || 0);
});
UserScheduleStore.subscribe('activeIndex', async ({ newValue }) => {
UserScheduleStore.listen('activeIndex', async ({ newValue }) => {
const schedules = await UserScheduleStore.get('schedules');
const numCourses = schedules[newValue]?.courses?.length;
updateBadgeText(numCourses || 0);

View File

@@ -1,6 +1,6 @@
import openNewTab from '@background/util/openNewTab';
import type { MessageHandler } from '@chrome-extension-toolkit';
import type CESMessage from '@shared/messages/CESMessage';
import type { MessageHandler } from 'chrome-extension-toolkit';
const CESFall2023Url = 'https://utexas.bluera.com/utexas/rpvl.aspx?rid=d3db767b-049f-46c5-9a67-29c21c29c580&regl=en-US';

View File

@@ -1,5 +1,5 @@
import type { MessageHandler } from '@chrome-extension-toolkit';
import type BrowserActionMessages from '@shared/messages/BrowserActionMessages';
import type { MessageHandler } from 'chrome-extension-toolkit';
const browserActionHandler: MessageHandler<BrowserActionMessages> = {
disableBrowserAction({ sender, sendResponse }) {

View File

@@ -1,14 +1,14 @@
import type { TabWithId } from '@background/util/openNewTab';
import openNewTab from '@background/util/openNewTab';
import type { MessageHandler } from '@chrome-extension-toolkit';
import { tabs } from '@shared/messages';
import type { CalendarBackgroundMessages } from '@shared/messages/CalendarMessages';
import { OptionsStore } from '@shared/storage/OptionsStore';
import { CRX_PAGES } from '@shared/types/CRXPages';
import type { MessageHandler } from 'chrome-extension-toolkit';
const getAllTabInfos = async () => {
const openTabs = (await chrome.tabs.query({})).filter((tab): tab is TabWithId => tab.id !== undefined);
const results = await Promise.allSettled(openTabs.map(tab => tabs.getTabInfo({ tabId: tab.id })));
const results = await Promise.allSettled(openTabs.map(tab => tabs.getTabInfo(undefined, tab.id)));
type TabInfo = PromiseFulfilledResult<Awaited<ReturnType<typeof tabs.getTabInfo>>>;
return results
@@ -34,7 +34,7 @@ const calendarBackgroundHandler: MessageHandler<CalendarBackgroundMessages> = {
await chrome.tabs.update(tabid, { active: true });
await chrome.windows.update(openCalendarTabInfo.tab.windowId, { focused: true, drawAttention: true });
if (uniqueId !== undefined) await tabs.openCoursePopup({ uniqueId }, { tabId: tabid });
if (uniqueId !== undefined) await tabs.openCoursePopup({ uniqueId }, tabid);
sendResponse(openCalendarTabInfo.tab);
} else {

View File

@@ -1,6 +1,6 @@
import type { MessageHandler } from '@chrome-extension-toolkit';
import type GitHubStatsMessages from '@shared/messages/GitHubStatsMessages';
import { GitHubStatsService } from '@views/lib/getGitHubStats';
import type { MessageHandler } from 'chrome-extension-toolkit';
const gitHubStatsService = new GitHubStatsService();

View File

@@ -1,6 +1,6 @@
import type { MessageHandler } from '@chrome-extension-toolkit';
import openNewTab from '@pages/background/util/openNewTab';
import type TabManagementMessages from '@shared/messages/TabManagementMessages';
import type { MessageHandler } from 'chrome-extension-toolkit';
const tabManagementHandler: MessageHandler<TabManagementMessages> = {
getTabId({ sendResponse, sender }) {

View File

@@ -1,4 +1,3 @@
import type { MessageHandler } from '@chrome-extension-toolkit';
import addCourse from '@pages/background/lib/addCourse';
import clearCourses from '@pages/background/lib/clearCourses';
import createSchedule from '@pages/background/lib/createSchedule';
@@ -10,6 +9,7 @@ import switchSchedule from '@pages/background/lib/switchSchedule';
import type { UserScheduleMessages } from '@shared/messages/UserScheduleMessages';
import { Course } from '@shared/types/Course';
import { validateLoginStatus } from '@shared/util/checkLoginStatus';
import type { MessageHandler } from 'chrome-extension-toolkit';
const userScheduleHandler: MessageHandler<UserScheduleMessages> = {
addCourse({ data, sendResponse }) {

View File

@@ -1,7 +1,7 @@
import type { Serialized } from '@chrome-extension-toolkit';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { UserSchedule } from '@shared/types/UserSchedule';
import { generateRandomId } from '@shared/util/random';
import type { Serialized } from 'chrome-extension-toolkit';
/**
* Creates a new schedule with the given name

View File

@@ -1,6 +1,6 @@
import type { Serialized } from '@chrome-extension-toolkit';
import { Course } from '@shared/types/Course';
import type { UserSchedule } from '@shared/types/UserSchedule';
import type { Serialized } from 'chrome-extension-toolkit';
import addCourse from './addCourse';
import createSchedule from './createSchedule';

View File

@@ -1,4 +1,3 @@
import { MessageListener } from '@chrome-extension-toolkit';
import type TabInfoMessages from '@shared/messages/TabInfoMessages';
import Calendar from '@views/components/calendar/Calendar';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
@@ -6,6 +5,7 @@ import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'
import { MigrationDialog } from '@views/components/common/MigrationDialog';
import { WhatsNewDialog } from '@views/components/common/WhatsNewPopup';
import SentryProvider from '@views/contexts/SentryContext';
import { MessageListener } from 'chrome-extension-toolkit';
import useKC_DABR_WASM from 'kc-dabr-wasm';
import React, { useEffect } from 'react';

View File

@@ -1,7 +1,9 @@
import type { GitHubStatsResult } from '@shared/types/GitHubStats';
import type { GitHubStatsService } from '@views/lib/getGitHubStats';
/* eslint-disable jsdoc/require-jsdoc */
export type GitHubStatsResult = Awaited<ReturnType<GitHubStatsService['fetchGitHubStats']>>;
export default interface GitHubStatsMessages {
/**
* Fetch GitHub statistics for all contributors

View File

@@ -1,4 +1,4 @@
import { createMessenger } from '@chrome-extension-toolkit';
import { createMessenger } from 'chrome-extension-toolkit';
import type BrowserActionMessages from './BrowserActionMessages';
import type { CalendarBackgroundMessages, CalendarTabMessages } from './CalendarMessages';

View File

@@ -1,5 +1,5 @@
import { createLocalStore } from '@chrome-extension-toolkit';
import type { CachedData } from '@shared/types/CachedData';
import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
interface ICacheStore {
github: Record<string, CachedData<unknown>>;
@@ -8,8 +8,8 @@ interface ICacheStore {
/**
* A store that is used for storing cached data such as GitHub contributors
*/
export const CacheStore = createLocalStore<ICacheStore>('CacheStore', {
export const CacheStore = createLocalStore<ICacheStore>({
github: {},
});
// debugStore({ cacheStore: CacheStore });
debugStore({ cacheStore: CacheStore });

View File

@@ -1,4 +1,4 @@
import { createLocalStore } from '@chrome-extension-toolkit';
import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
/**
* A store that is used to store data that is only relevant during development
@@ -18,7 +18,7 @@ interface IDevStore {
reloadTabId?: number;
}
export const DevStore = createLocalStore<IDevStore>('DevStore', {
export const DevStore = createLocalStore<IDevStore>({
isDeveloper: false,
debugTabId: undefined,
isTabReloading: true,
@@ -27,4 +27,4 @@ export const DevStore = createLocalStore<IDevStore>('DevStore', {
reloadTabId: undefined,
});
// debugStore({ devStore: DevStore });
debugStore({ devStore: DevStore });

View File

@@ -1,4 +1,4 @@
import { createLocalStore } from '@chrome-extension-toolkit';
import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
/**
* A store that is used for storing user options
@@ -12,10 +12,10 @@ interface IExtensionStore {
lastWhatsNewPopupVersion: number;
}
export const ExtensionStore = createLocalStore<IExtensionStore>('ExtensionStore', {
export const ExtensionStore = createLocalStore<IExtensionStore>({
version: chrome.runtime.getManifest().version,
lastUpdate: Date.now(),
lastWhatsNewPopupVersion: 0,
});
// debugStore({ ExtensionStore });
debugStore({ ExtensionStore });

View File

@@ -1,4 +1,4 @@
import { createSyncStore } from '@chrome-extension-toolkit';
import { createSyncStore, debugStore } from 'chrome-extension-toolkit';
/**
* A store that is used for storing user options
@@ -28,7 +28,7 @@ export interface IOptionsStore {
allowMoreSchedules: boolean;
}
export const OptionsStore = createSyncStore<IOptionsStore>('OptionsStore', {
export const OptionsStore = createSyncStore<IOptionsStore>({
enableCourseStatusChips: false,
enableHighlightConflicts: true,
enableScrollToLoad: true,
@@ -58,4 +58,4 @@ export const initSettings = async () =>
// Clothing retailer right
// debugStore({ OptionsStore });
debugStore({ OptionsStore });

View File

@@ -1,5 +1,5 @@
import { createLocalStore } from '@chrome-extension-toolkit';
import { UserSchedule } from '@shared/types/UserSchedule';
import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
import { generateRandomId } from '../util/random';
@@ -11,7 +11,7 @@ interface IUserScheduleStore {
/**
* A store that is used for storing user schedules (and the active schedule)
*/
export const UserScheduleStore = createLocalStore<IUserScheduleStore>('UserScheduleStore', {
export const UserScheduleStore = createLocalStore<IUserScheduleStore>({
schedules: [
new UserSchedule({
courses: [],
@@ -24,4 +24,4 @@ export const UserScheduleStore = createLocalStore<IUserScheduleStore>('UserSched
activeIndex: 0,
});
// debugStore({ userScheduleStore: UserScheduleStore });
debugStore({ userScheduleStore: UserScheduleStore });

View File

@@ -1,5 +1,5 @@
import type { Serialized } from '@chrome-extension-toolkit';
import { getCourseColors } from '@shared/util/colors';
import type { Serialized } from 'chrome-extension-toolkit';
import type { CourseMeeting } from './CourseMeeting';
import { CourseSchedule } from './CourseSchedule';

View File

@@ -1,4 +1,4 @@
import type { Serialized } from '@chrome-extension-toolkit';
import type { Serialized } from 'chrome-extension-toolkit';
/**
* a map of the days of the week that a class is taught, and the corresponding abbreviation

View File

@@ -1,4 +1,4 @@
import type { Serialized } from '@chrome-extension-toolkit';
import type { Serialized } from 'chrome-extension-toolkit';
import type { Day } from './CourseMeeting';
import { CourseMeeting, DAY_MAP } from './CourseMeeting';

View File

@@ -1,40 +0,0 @@
/* eslint-disable jsdoc/require-jsdoc */
export type TeamMember = {
name: string;
role: string[];
githubUsername: string;
};
export type GitHubStats = {
commits: number;
linesAdded: number;
linesDeleted: number;
mergedPRs?: number;
};
export type GitHubStatsResult = {
adminGitHubStats: Record<string, GitHubStats>;
userGitHubStats: Record<string, GitHubStats>;
contributors: string[];
names: Record<string, string>;
dataFetched: Date;
lastUpdated: Date;
isCached: boolean;
};
export type ContributorStats = {
total: number;
weeks: { w: number; a: number; d: number; c: number }[];
author: { login: string };
};
export type ContributorUser = {
name: string | undefined;
};
export type FetchResult<T> = {
data: T;
dataFetched: Date;
lastUpdated: Date;
isCached: boolean;
};

View File

@@ -1,5 +1,5 @@
import type { Serialized } from '@chrome-extension-toolkit';
import { capitalize } from '@shared/util/string';
import type { Serialized } from 'chrome-extension-toolkit';
/**
* A type representing an instructor for a course (who teaches it)

View File

@@ -1,12 +1,7 @@
import type { Serialized } from '@chrome-extension-toolkit';
import type { Serialized } from 'chrome-extension-toolkit';
import { generateRandomId } from '../util/random';
import { Course } from './Course';
export interface Wallpaper {
type: string;
/** Image url will be here */
data?: string;
}
/**
* Represents a user's schedule that is stored in the extension
@@ -18,8 +13,6 @@ export class UserSchedule {
hours: number;
/** Unix timestamp of when the schedule was last updated */
updatedAt: number;
/** Wallpaper for this schedule */
wallpaper?: Wallpaper;
constructor(schedule: Serialized<UserSchedule>) {
this.courses = schedule.courses.map(c => new Course(c));

View File

@@ -1,4 +1,4 @@
import type { Serialized } from '@chrome-extension-toolkit';
import type { Serialized } from 'chrome-extension-toolkit';
import { theme } from 'unocss/preset-mini';
import type { HexColor, HSL, Lab, RGB, sRGB } from '../types/Color';

View File

@@ -1,4 +1,3 @@
import type { Serialized } from '@chrome-extension-toolkit';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { UserSchedule } from '@shared/types/UserSchedule';
import { generateRandomId } from '@shared/util/random';
@@ -8,6 +7,7 @@ import ScheduleDropdown from '@views/components/common/ScheduleDropdown';
import ScheduleListItem from '@views/components/common/ScheduleListItem';
import { SortableList } from '@views/components/common/SortableList';
import useSchedules, { getActiveSchedule, switchSchedule } from '@views/hooks/useSchedules';
import type { Serialized } from 'chrome-extension-toolkit';
import React, { useEffect } from 'react';
import { exampleSchedule } from '../injected/mocked';

View File

@@ -37,7 +37,7 @@ export default function PopupMain(): JSX.Element {
initAllSettings();
const l1 = OptionsStore.subscribe('enableCourseStatusChips', async ({ newValue }) => {
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
setEnableCourseStatusChips(newValue);
// console.log('enableCourseStatusChips', newValue);
});
@@ -48,7 +48,7 @@ export default function PopupMain(): JSX.Element {
// });
return () => {
OptionsStore.unsubscribe(l1);
OptionsStore.removeListener(l1);
// OptionsStore.removeListener(l2);
};
}, []);

View File

@@ -1,4 +1,3 @@
import { MessageListener } from '@chrome-extension-toolkit';
import { Sidebar } from '@phosphor-icons/react';
import type { CalendarTabMessages } from '@shared/messages/CalendarMessages';
import { OptionsStore } from '@shared/storage/OptionsStore';
@@ -17,6 +16,7 @@ import { CalendarContext } from '@views/contexts/CalendarContext';
import useCourseFromUrl from '@views/hooks/useCourseFromUrl';
import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule';
import useWhatsNewPopUp from '@views/hooks/useWhatsNew';
import { MessageListener } from 'chrome-extension-toolkit';
import clsx from 'clsx';
import type { ReactNode } from 'react';
import React, { useEffect, useState } from 'react';
@@ -28,31 +28,6 @@ import { LargeLogo } from '../common/LogoIcon';
import Text from '../common/Text/Text';
import CalendarFooter from './CalendarFooter';
import DiningAppPromo from './DiningAppPromo';
import { Wallpaper } from 'src/shared/types/UserSchedule';
function getWallpaper(wallpaper?: Wallpaper): React.CSSProperties {
if (!wallpaper || !wallpaper.data || wallpaper.type === 'none') {
return { backgroundColor: 'white' };
}
let imageUrl: string;
// get wallpaper image from storage, or just default to white
if (wallpaper.type === 'custom' && wallpaper.data) {
imageUrl = wallpaper.data;
} else {
// Fallback to white background
return { backgroundColor: 'white' };
}
return {
backgroundImage: `url(${imageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundOrigin: 'border-box',
};
}
/**
* Calendar page component
@@ -121,7 +96,7 @@ export default function Calendar(): ReactNode {
return (
<CalendarContext.Provider value>
<div className='h-full w-full flex flex-col' style={getWallpaper(activeSchedule.wallpaper)}>
<div className='h-full w-full flex flex-col'>
<div className='screenshot:calendar-target h-screen flex overflow-auto'>
<div
className={clsx(

View File

@@ -61,13 +61,13 @@ export default function CalendarCourseCell({
useEffect(() => {
initSettings().then(({ enableCourseStatusChips }) => setEnableCourseStatusChips(enableCourseStatusChips));
const l1 = OptionsStore.subscribe('enableCourseStatusChips', async ({ newValue }) => {
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
setEnableCourseStatusChips(newValue);
// console.log('enableCourseStatusChips', newValue);
});
return () => {
OptionsStore.unsubscribe(l1);
OptionsStore.removeListener(l1);
};
}, []);

View File

@@ -1,5 +1,5 @@
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
import { CalendarDots, Export, FileCode, FilePng, FileText, Sidebar } from '@phosphor-icons/react';
import { CalendarDots, Export, FileCode, FilePng, Sidebar } from '@phosphor-icons/react';
import styles from '@views/components/calendar/CalendarHeader/CalendarHeader.module.scss';
import { Button } from '@views/components/common/Button';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
@@ -11,7 +11,7 @@ import useSchedules from '@views/hooks/useSchedules';
import clsx from 'clsx';
import React from 'react';
import { handleExportJson, saveAsCal, saveAsText, saveCalAsPng } from '../utils';
import { handleExportJson, saveAsCal, saveCalAsPng } from '../utils';
interface CalendarHeaderProps {
sidebarOpen?: boolean;
@@ -111,18 +111,6 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
Save as .json
</Button>
</MenuItem>
<MenuItem>
<Button
className='w-full flex justify-start'
onClick={saveAsText}
color='ut-black'
size='small'
variant='minimal'
icon={FileText}
>
Save as .txt
</Button>
</MenuItem>
{/* <MenuItem>
<Button color='ut-black' size='small' variant='minimal' icon={FileTxt}>
Export Unique IDs

View File

@@ -1,8 +1,8 @@
import type { Serialized } from '@chrome-extension-toolkit';
import { tz } from '@date-fns/tz';
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import type { Serialized } from 'chrome-extension-toolkit';
import { format as formatDate, parseISO } from 'date-fns';
import {
chatterjeeCS429Course,

View File

@@ -1,4 +1,3 @@
import type { Serialized } from '@chrome-extension-toolkit';
import { tz, TZDate } from '@date-fns/tz';
import exportSchedule from '@pages/background/lib/exportSchedule';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
@@ -9,6 +8,7 @@ import type { UserSchedule } from '@shared/types/UserSchedule';
import { downloadBlob } from '@shared/util/downloadBlob';
import { englishStringifyList } from '@shared/util/string';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import type { Serialized } from 'chrome-extension-toolkit';
import type { DateArg, Day } from 'date-fns';
import {
addDays,
@@ -245,27 +245,6 @@ export const scheduleToIcsString = (schedule: Serialized<UserSchedule>) => {
return icsString;
};
/**
* Returns the provided schedule in a human readable/copyable text format
* @param schedule - The schedule object
* @returns
*/
export const scheduleToText = (schedule: Serialized<UserSchedule>) => {
const lines: string[] = [];
lines.push(`Schedule: ${schedule.name}`);
lines.push('');
for (const c of schedule.courses) {
lines.push(c.fullName);
lines.push(`${c.creditHours} Credit Hours`);
lines.push(`${c.uniqueId}`);
lines.push('');
}
return lines.join('\n');
};
/**
* Saves the current schedule as a calendar file in the iCalendar format (ICS).
* Fetches the current active schedule and converts it into an ICS string.
@@ -283,25 +262,6 @@ export const saveAsCal = async () => {
downloadBlob(icsString, 'CALENDAR', 'schedule.ics');
};
/**
* Save current schedule as a plain text file consisting of
* Course Name - Course ID
* Course Time
* Unique Number
* Line Break
* Repeat
*/
export const saveAsText = async () => {
const schedule = await getSchedule();
if (!schedule) {
throw new Error('No schedule found');
}
const scheduleText = scheduleToText(schedule);
downloadBlob(scheduleText, 'TEXT', 'schedule.txt');
};
/**
* Saves current schedule to JSON that can be imported on other devices.
* @param id - Provided schedule ID to download

View File

@@ -61,7 +61,7 @@ export default function PopupCourseBlock({ className, course, colors }: PopupCou
initAllSettings();
const l1 = OptionsStore.subscribe('enableCourseStatusChips', async ({ newValue }) => {
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
setEnableCourseStatusChips(newValue);
});
@@ -77,7 +77,7 @@ export default function PopupCourseBlock({ className, course, colors }: PopupCou
});
return () => {
OptionsStore.unsubscribe(l1);
OptionsStore.removeListener(l1);
};
}, []);

View File

@@ -54,19 +54,6 @@ export default function ScheduleListItem({ schedule, onClick }: ScheduleListItem
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Delete' && !isEditing) {
// Check if any popups/dialogs are open by looking for open menu elements
const openMenus = document.querySelectorAll('[data-headlessui-state="open"]');
const openDialogs = document.querySelectorAll('[role="dialog"]');
if (openMenus.length === 0 && openDialogs.length === 0) {
e.preventDefault();
handleDelete();
}
}
};
const editorRef = React.useRef<HTMLInputElement>(null);
useEffect(() => {
const editor = editorRef.current;
@@ -121,10 +108,6 @@ export default function ScheduleListItem({ schedule, onClick }: ScheduleListItem
};
const handleDelete = () => {
if (schedule.courses.length <= 0) {
deleteSchedule(schedule.id);
return;
}
showDialog({
title: 'Delete schedule?',
description: (
@@ -159,7 +142,7 @@ export default function ScheduleListItem({ schedule, onClick }: ScheduleListItem
};
return (
<div className='h-7.5 rounded bg-white' tabIndex={0} onKeyDown={handleKeyDown}>
<div className='h-7.5 rounded bg-white'>
<div className='h-full w-full flex cursor-pointer items-center gap-[1px] text-ut-burntorange'>
{IS_STORYBOOK ? (
<DotsSixVertical
@@ -199,9 +182,6 @@ export default function ScheduleListItem({ schedule, onClick }: ScheduleListItem
if (e.key === 'Escape') {
setIsEditing(false);
}
if (e.key === 'Delete') {
e.stopPropagation();
}
}}
onBlur={handleBlur}
ref={editorRef}

View File

@@ -1,44 +0,0 @@
import clsx from 'clsx';
import type { PropsWithChildren, ReactNode } from 'react';
import React from 'react';
interface TooltipProps {
className?: string;
contentClassName?: string;
content: ReactNode;
offsetX: number;
offsetY: number;
maxWidth?: number;
}
/**
* Tooltip that displays content on hover
*/
export default function Tooltip({
className,
contentClassName,
content,
offsetX,
offsetY,
maxWidth,
children,
}: PropsWithChildren<TooltipProps>): JSX.Element {
return (
<span className={clsx('relative inline-flex group', className)}>
{children}
<span
className={clsx(
'pointer-events-none absolute rounded-md bg-white px-3 py-2 text-xs invisible opacity-0 transition-opacity group-hover:visible group-hover:opacity-100 whitespace-normal break-words',
contentClassName
)}
style={{
marginTop: offsetY,
marginLeft: offsetX,
maxWidth,
}}
>
{content}
</span>
</span>
);
}

View File

@@ -3,7 +3,6 @@ import type { Distribution, LetterGrade } from '@shared/types/Distribution';
import { extendedColors } from '@shared/types/ThemeColors';
import Link from '@views/components/common/Link';
import Text from '@views/components/common/Text/Text';
import Tooltip from '@views/components/common/Tooltip';
import {
NoDataError,
queryAggregateDistribution,
@@ -13,12 +12,10 @@ import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import type { ChangeEvent } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import Skeleton from 'react-loading-skeleton';
const UT_GRADE_DISTRIBUTION_URL = 'https://reports.utexas.edu/spotlight-data/ut-course-grade-distributions';
const TOOLTIP_CONTENT =
"The 'Other' grade category includes all non-standard letter grades, including: In Progress, Incomplete, Permanent Incomplete, Oblit, Q-Drop, Withdrawn, Credit, No Credit, Satisfactory, Unsatisfactory, and Registered on CR/F or CR/NC basis.";
interface GradeDistributionProps {
course: Course;
}
@@ -129,25 +126,6 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
lineHeight: 'normal',
fontStyle: 'normal',
},
useHTML: true,
formatter() {
// eslint-disable-next-line react/no-this-in-sfc
const val = `${this.value}`;
return val === 'Other'
? renderToStaticMarkup(
<Tooltip
content={TOOLTIP_CONTENT}
className='underline'
offsetX={-425}
offsetY={-175}
maxWidth={500}
>
Other
</Tooltip>
)
: val;
},
},
title: {
text: 'Grades',
@@ -157,7 +135,6 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
fontWeight: '400',
},
},
categories: ['A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F', 'Other'],
tickInterval: 1,
tickWidth: 1,

View File

@@ -34,14 +34,14 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
setHighlightConflicts(enableHighlightConflicts);
});
const l1 = OptionsStore.subscribe('enableHighlightConflicts', async ({ newValue }) => {
const l1 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => {
setHighlightConflicts(newValue);
// console.log('enableHighlightConflicts', newValue);
});
// Remove listeners when the component is unmounted
return () => {
OptionsStore.unsubscribe(l1);
OptionsStore.removeListener(l1);
};
}, []);

View File

@@ -1,14 +0,0 @@
import React from 'react';
/**
* Lightweight skeleton placeholder for contributor cards while data loads
*/
export const ContributorCardSkeleton: React.FC = () => (
<div className='animate-pulse border border-gray-300 rounded bg-ut-gray/10 p-4'>
<div className='mb-2 h-4 w-3/4 rounded bg-gray-300' />
<div className='mb-1 h-3 w-1/2 rounded bg-gray-300' />
<div className='h-3 w-1/4 rounded bg-gray-300' />
</div>
);
export default ContributorCardSkeleton;

View File

@@ -20,7 +20,7 @@ import useChangelog from '@views/hooks/useChangelog';
import useSchedules from '@views/hooks/useSchedules';
import { GitHubStatsService, LONGHORN_DEVELOPERS_ADMINS, LONGHORN_DEVELOPERS_SWE } from '@views/lib/getGitHubStats';
// Misc
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// Icons
import IconoirGitFork from '~icons/iconoir/git-fork';
@@ -29,7 +29,6 @@ import { useMigrationDialog } from '../common/MigrationDialog';
import { AdvancedSettings } from './AdvancedSettings';
import { DEV_MODE_CLICK_TARGET, INCLUDE_MERGED_PRS, STATS_TOGGLE_KEY } from './constants';
import { ContributorCard } from './ContributorCard';
import { ContributorCardSkeleton } from './ContributorCardSkeleton';
import DevMode from './DevMode';
import { useBirthdayCelebration } from './useBirthdayCelebration';
import { useDevMode } from './useDevMode';
@@ -63,9 +62,6 @@ export default function Settings(): JSX.Element {
const [devMode, toggleDevMode] = useDevMode(DEV_MODE_CLICK_TARGET);
const { showParticles, particlesInit, particlesOptions, triggerCelebration, isBirthday } = useBirthdayCelebration();
// Stable skeleton ids to avoid using array index as keys
const skeletonIdsRef = useRef<string[]>(Array.from({ length: 8 }, (_, i) => `skeleton-${i}`));
// Initialize settings and listeners
useEffect(() => {
const fetchGitHubStats = async () => {
@@ -98,23 +94,23 @@ export default function Settings(): JSX.Element {
};
// Listeners
const ds_l1 = DevStore.subscribe('isDeveloper', async ({ newValue }) => {
const ds_l1 = DevStore.listen('isDeveloper', async ({ newValue }) => {
setIsDeveloper(newValue);
});
const l1 = OptionsStore.subscribe('enableHighlightConflicts', async ({ newValue }) => {
const l1 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => {
setHighlightConflicts(newValue);
});
const l2 = OptionsStore.subscribe('enableScrollToLoad', async ({ newValue }) => {
const l2 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => {
setLoadAllCourses(newValue);
});
const l3 = OptionsStore.subscribe('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
const l3 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
setCalendarNewTab(newValue);
});
const l4 = OptionsStore.subscribe('allowMoreSchedules', async ({ newValue }) => {
const l4 = OptionsStore.listen('allowMoreSchedules', async ({ newValue }) => {
setIncreaseScheduleLimit(newValue);
});
@@ -125,11 +121,11 @@ export default function Settings(): JSX.Element {
initAndSetSettings();
return () => {
OptionsStore.unsubscribe(l1);
OptionsStore.unsubscribe(l2);
OptionsStore.unsubscribe(l3);
OptionsStore.unsubscribe(l4);
DevStore.unsubscribe(ds_l1);
OptionsStore.removeListener(l1);
OptionsStore.removeListener(l2);
OptionsStore.removeListener(l3);
OptionsStore.removeListener(l4);
DevStore.removeListener(ds_l1);
window.removeEventListener('keydown', handleKeyPress);
};
}, [gitHubStatsService]);
@@ -365,6 +361,7 @@ export default function Settings(): JSX.Element {
))}
</div>
</section>
<section className='my-8'>
<h2 className='mb-4 text-xl text-ut-black font-semibold'>UTRP CONTRIBUTORS</h2>
<div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3 xl:grid-cols-3'>
@@ -379,9 +376,7 @@ export default function Settings(): JSX.Element {
includeMergedPRs={INCLUDE_MERGED_PRS}
/>
))}
{githubStats === null
? skeletonIdsRef.current.slice(0, 8).map(id => <ContributorCardSkeleton key={id} />)
: additionalContributors.map(username => (
{additionalContributors.map(username => (
<ContributorCard
key={username}
name={githubStats!.names[username] || username}

View File

@@ -7,7 +7,7 @@ export const DEV_MODE_CLICK_TARGET = 5;
export const DEV_MODE_CLICK_TIMEOUT = 5000;
export const DEV_MODE_CLICK_INTERVAL = 500;
// LHD Birthday: January 9th, 2024
// LHD Birthday: January 9th, 2025
export const LHD_BIRTHDAY = { month: 0, day: 9 };
export const BIRTHDAY_CELEBRATION_DURATION = 5000;
export const BIRTHDAY_CELEBRATION_DEBOUNCE = 2000;

View File

@@ -33,13 +33,13 @@ export function useEnforceScheduleLimit(): () => boolean {
}
})();
const listener = OptionsStore.subscribe('allowMoreSchedules', async ({ newValue }) => {
const listener = OptionsStore.listen('allowMoreSchedules', async ({ newValue }) => {
setAllowMoreSchedules(newValue);
});
return () => {
mounted = false;
OptionsStore.unsubscribe(listener);
OptionsStore.removeListener(listener);
};
}, []);

View File

@@ -48,19 +48,19 @@ export default function useSchedules(): [active: UserSchedule, schedules: UserSc
}
useEffect(() => {
const l1 = UserScheduleStore.subscribe('schedules', ({ newValue }) => {
const l1 = UserScheduleStore.listen('schedules', ({ newValue }) => {
schedulesCache = newValue.map(s => new UserSchedule(s));
setSchedules(schedulesCache);
});
const l2 = UserScheduleStore.subscribe('activeIndex', ({ newValue }) => {
const l2 = UserScheduleStore.listen('activeIndex', ({ newValue }) => {
activeIndexCache = newValue;
setActiveIndex(newValue);
});
return () => {
UserScheduleStore.unsubscribe(l1);
UserScheduleStore.unsubscribe(l2);
UserScheduleStore.removeListener(l1);
UserScheduleStore.removeListener(l2);
};
}, []);

View File

@@ -1,4 +1,4 @@
import { createUseMessage } from '@chrome-extension-toolkit';
import type { TAB_MESSAGES } from '@shared/messages';
import { createUseMessage } from 'chrome-extension-toolkit';
export const useTabMessage = createUseMessage<TAB_MESSAGES>();

View File

@@ -9,12 +9,12 @@ export default function useVersion(): string {
const [version, setVersion] = useState<string>('');
useEffect(() => {
const listener = ExtensionStore.subscribe('version', ({ newValue }) => {
const listener = ExtensionStore.listen('version', ({ newValue }) => {
setVersion(newValue);
});
return () => {
ExtensionStore.unsubscribe(listener);
ExtensionStore.removeListener(listener);
};
}, []);

View File

@@ -1,4 +1,4 @@
import { ContextInvalidated, createShadowDOM, onContextInvalidated } from '@chrome-extension-toolkit';
import { ContextInvalidated, createShadowDOM, onContextInvalidated } from 'chrome-extension-toolkit';
import React from 'react';
import CourseCatalogMain from './components/CourseCatalogMain';

View File

@@ -1,13 +1,37 @@
import { Octokit } from '@octokit/rest';
import { CacheStore } from '@shared/storage/CacheStore';
import type { CachedData } from '@shared/types/CachedData';
import type {
ContributorStats,
ContributorUser,
FetchResult,
GitHubStats,
TeamMember,
} from '@shared/types/GitHubStats';
// Types
type TeamMember = {
name: string;
role: string[];
githubUsername: string;
};
type GitHubStats = {
commits: number;
linesAdded: number;
linesDeleted: number;
mergedPRs?: number;
};
type ContributorStats = {
total: number;
weeks: { w: number; a: number; d: number; c: number }[];
author: { login: string };
};
type ContributorUser = {
name: string | undefined;
};
type FetchResult<T> = {
data: T;
dataFetched: Date;
lastUpdated: Date;
isCached: boolean;
};
// Constants
const CACHE_TTL = 1 * 60 * 60 * 1000; // 1 hour in milliseconds
@@ -67,8 +91,6 @@ export class GitHubStatsService {
private octokit: Octokit;
private cache: Record<string, CachedData<unknown>>;
private storageLock: Promise<void> = Promise.resolve();
constructor(githubToken?: string) {
this.octokit = githubToken ? new Octokit({ auth: githubToken }) : new Octokit();
this.cache = {} as Record<string, CachedData<unknown>>;
@@ -92,16 +114,7 @@ export class GitHubStatsService {
return null;
}
private async setCachedData<T>(key: string, data: T, persist = true): Promise<void> {
// get the current lock
const existingLock = this.storageLock;
// update the lock with a new promise
this.storageLock = (async () => {
// wait for current lock to finish
await existingLock;
// ensure cache is loaded before modifying
private async setCachedData<T>(key: string, data: T): Promise<void> {
if (Object.keys(this.cache).length === 0) {
const githubCache = await CacheStore.get('github');
if (githubCache && typeof githubCache === 'object') {
@@ -109,17 +122,9 @@ export class GitHubStatsService {
}
}
// update local memory
this.cache[key] = { data, dataFetched: Date.now() };
// only write to the physical storage API if persist is true
if (persist) {
await CacheStore.set('github', this.cache);
}
})();
return this.storageLock;
}
private async fetchWithRetry<T>(fetchFn: () => Promise<T>, retries: number = 3, delay: number = 5000): Promise<T> {
try {
@@ -177,7 +182,6 @@ export class GitHubStatsService {
private async fetchContributorNames(contributors: string[]): Promise<Record<string, string>> {
const names: Record<string, string> = {};
await Promise.all(
contributors.map(async contributor => {
const cacheKey = `contributor_name_${contributor}`;
@@ -194,17 +198,18 @@ export class GitHubStatsService {
if (data.name) {
name = data.name;
}
// Pass 'false' to avoid writing to disk for every single name
await this.setCachedData(cacheKey, name, false);
await this.setCachedData(cacheKey, name);
} catch (e) {
console.error(e);
}
}
names[contributor] = name;
})
);
return names;
}
private async fetchMergedPRsCount(username: string): Promise<FetchResult<number>> {
const cacheKey = `merged_prs_${username}`;
const cachedCount = await this.getCachedData<number>(cacheKey);
@@ -228,7 +233,7 @@ export class GitHubStatsService {
lastUpdated: new Date(),
isCached: false,
};
await this.setCachedData(cacheKey, fetchResult.data, false);
await this.setCachedData(cacheKey, fetchResult.data);
return fetchResult;
}
@@ -295,8 +300,6 @@ export class GitHubStatsService {
const names = await this.fetchContributorNames(contributors);
await CacheStore.set('github', this.cache);
return {
adminGitHubStats,
userGitHubStats,

View File

@@ -1,4 +1,4 @@
import { isExtensionPage, isExtensionPopup } from '@chrome-extension-toolkit';
import { isExtensionPage, isExtensionPopup } from 'chrome-extension-toolkit';
/**
* An enum that represents the different types of pages that we support

View File

@@ -1,4 +1,4 @@
import { createMessenger } from '@chrome-extension-toolkit';
import { createMessenger } from 'chrome-extension-toolkit';
type MyMessages = {
openNewTab: (data: { url: string }) => void;

View File

@@ -25,9 +25,8 @@
"@public/*": ["public/*"],
"@shared/*": ["src/shared/*"],
"@background/*": ["src/pages/background/*"],
"@views/*": ["src/views/*"],
"@chrome-extension-toolkit": ["src/lib/chrome-extension-toolkit"],
},
"@views/*": ["src/views/*"]
}
},
"include": [
"src",
@@ -43,6 +42,6 @@
".storybook",
"unocss.config.ts",
"gulpfile.js",
"@types",
],
"@types"
]
}

View File

@@ -226,7 +226,6 @@ export default defineConfig({
'@shared': resolve(root, 'shared'),
'@background': resolve(pagesDir, 'background'),
'@views': resolve(root, 'views'),
'@chrome-extension-toolkit': resolve(root, 'lib/chrome-extension-toolkit'),
},
},
server: {
@@ -285,7 +284,6 @@ export default defineConfig({
},
},
test: {
environment: 'jsdom',
coverage: {
provider: 'v8',
},