diff --git a/.gitignore b/.gitignore index 6bddc22..2fe0468 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ dist-ssr *.sw? gauntface-pin-it-extension.zip +coverage/ diff --git a/eslint.config.js b/eslint.config.js index b48947b..0c644cf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -47,6 +47,7 @@ export default tseslint.config( ignoreRestSiblings: true, }, ], + "no-console": "error", }, }, ); diff --git a/package.json b/package.json index ee19396..963d922 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,15 @@ "dev": "vite", "build": "vite build", "build:dev": "vite build --mode development", - "test": "npm run lint", - "test:ci": "npm run lint:ci", + "test": "npm run lint && npm run vitest", + "test:ci": "npm run lint:ci && npm run vitest:ci", "lint": "npm run eslint -- --fix && npm run prettier -- --write", "lint:ci": "npm run eslint && npm run prettier -- --check", "eslint": "eslint 'src/**/*.{js,ts,svelte}'", "prettier": "prettier 'src/**/*.{js,ts,svelte}'", "preview": "vite preview", + "vitest": "vitest --coverage", + "vitest:ci": "vitest run", "chrome-webstore-upload": "chrome-webstore-upload" }, "devDependencies": { @@ -24,16 +26,19 @@ "@sentry/cli": "^2.32.1", "@sveltejs/vite-plugin-svelte": "^3.1.1", "@tsconfig/svelte": "^5.0.4", + "@types/archiver": "^6.0.2", "@types/eslint__js": "^8.42.3", - "@types/lodash": "^4.17.4", + "@types/lodash-es": "^4.17.12", + "@types/semver": "^7.5.8", "@types/webextension-polyfill": "^0.10.7", + "@vitest/coverage-v8": "^2.0.5", "archiver": "^7.0.1", "chrome-webstore-upload-cli": "^3.1.0", "eslint": "^9.8.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.43.0", "globals": "^15.9.0", - "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "prettier": "^3.3.0", "prettier-plugin-svelte": "^3.2.3", "semver": "^7.6.2", @@ -44,6 +49,7 @@ "typescript": "^5.5.4", "typescript-eslint": "^8.0.0", "vite": "^5.2.12", + "vitest": "^2.0.5", "webextension-polyfill": "^0.12.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fdc018..8ef9112 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,19 +22,28 @@ importers: version: 2.33.1 '@sveltejs/vite-plugin-svelte': specifier: ^3.1.1 - version: 3.1.1(svelte@4.2.18)(vite@5.4.0) + version: 3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@22.2.0)) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.4 + '@types/archiver': + specifier: ^6.0.2 + version: 6.0.2 '@types/eslint__js': specifier: ^8.42.3 version: 8.42.3 - '@types/lodash': - specifier: ^4.17.4 - version: 4.17.7 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 '@types/webextension-polyfill': specifier: ^0.10.7 version: 0.10.7 + '@vitest/coverage-v8': + specifier: ^2.0.5 + version: 2.0.5(vitest@2.0.5(@types/node@22.2.0)) archiver: specifier: ^7.0.1 version: 7.0.1 @@ -53,7 +62,7 @@ importers: globals: specifier: ^15.9.0 version: 15.9.0 - lodash: + lodash-es: specifier: ^4.17.21 version: 4.17.21 prettier: @@ -85,7 +94,10 @@ importers: version: 8.0.1(eslint@9.9.0)(typescript@5.5.4) vite: specifier: ^5.2.12 - version: 5.4.0 + version: 5.4.0(@types/node@22.2.0) + vitest: + specifier: ^2.0.5 + version: 2.0.5(@types/node@22.2.0) webextension-polyfill: specifier: ^0.12.0 version: 0.12.0 @@ -96,6 +108,26 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@babel/helper-string-parser@7.24.8': + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.25.3': + resolution: {integrity: sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.25.2': + resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -275,6 +307,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -488,6 +524,9 @@ packages: '@tsconfig/svelte@5.0.4': resolution: {integrity: sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q==} + '@types/archiver@6.0.2': + resolution: {integrity: sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==} + '@types/eslint@9.6.0': resolution: {integrity: sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==} @@ -500,12 +539,24 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + '@types/lodash@4.17.7': resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} + '@types/node@22.2.0': + resolution: {integrity: sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==} + '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} + '@types/readdir-glob@1.1.5': + resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} + + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/webextension-polyfill@0.10.7': resolution: {integrity: sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw==} @@ -566,6 +617,29 @@ packages: resolution: {integrity: sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/coverage-v8@2.0.5': + resolution: {integrity: sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==} + peerDependencies: + vitest: 2.0.5 + + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/runner@2.0.5': + resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} + + '@vitest/snapshot@2.0.5': + resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} + + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -625,6 +699,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} @@ -668,14 +746,26 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -749,6 +839,10 @@ packages: supports-color: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -872,6 +966,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -922,6 +1020,13 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -935,6 +1040,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -961,10 +1070,17 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1018,16 +1134,39 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1074,12 +1213,18 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + lru-cache@10.2.0: resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} engines: {node: 14 || >=16.14} @@ -1090,6 +1235,13 @@ packages: magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magicast@0.3.4: + resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} @@ -1097,6 +1249,9 @@ packages: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1105,6 +1260,10 @@ packages: resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -1127,6 +1286,10 @@ packages: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -1159,9 +1322,17 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1174,6 +1345,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1190,14 +1364,29 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-scurry@1.10.1: resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} engines: {node: '>=16 || 14 >=14.17'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} @@ -1355,6 +1544,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -1371,6 +1563,12 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + streamx@2.16.1: resolution: {integrity: sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==} @@ -1396,6 +1594,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -1510,9 +1712,32 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinypool@1.0.0: + resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} + engines: {node: '>=14.0.0'} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1547,12 +1772,20 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@6.13.0: + resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite-node@2.0.5: + resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite@5.4.0: resolution: {integrity: sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1592,6 +1825,31 @@ packages: vite: optional: true + vitest@2.0.5: + resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.0.5 + '@vitest/ui': 2.0.5 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + webextension-polyfill@0.12.0: resolution: {integrity: sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==} @@ -1606,6 +1864,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -1643,6 +1906,22 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@babel/helper-string-parser@7.24.8': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/parser@7.25.3': + dependencies: + '@babel/types': 7.25.2 + + '@babel/types@7.25.2': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@bcoe/v8-coverage@0.2.3': {} + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -1760,6 +2039,8 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -1929,31 +2210,35 @@ snapshots: dependencies: '@sentry/types': 8.25.0 - '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0))(svelte@4.2.18)(vite@5.4.0)': + '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@22.2.0)))(svelte@4.2.18)(vite@5.4.0(@types/node@22.2.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.18)(vite@5.4.0) + '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@22.2.0)) debug: 4.3.5 svelte: 4.2.18 - vite: 5.4.0 + vite: 5.4.0(@types/node@22.2.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0)': + '@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@22.2.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0))(svelte@4.2.18)(vite@5.4.0) + '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.4.0(@types/node@22.2.0)))(svelte@4.2.18)(vite@5.4.0(@types/node@22.2.0)) debug: 4.3.5 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.10 svelte: 4.2.18 svelte-hmr: 0.16.0(svelte@4.2.18) - vite: 5.4.0 - vitefu: 0.2.5(vite@5.4.0) + vite: 5.4.0(@types/node@22.2.0) + vitefu: 0.2.5(vite@5.4.0(@types/node@22.2.0)) transitivePeerDependencies: - supports-color '@tsconfig/svelte@5.0.4': {} + '@types/archiver@6.0.2': + dependencies: + '@types/readdir-glob': 1.1.5 + '@types/eslint@9.6.0': dependencies: '@types/estree': 1.0.5 @@ -1967,10 +2252,24 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.7 + '@types/lodash@4.17.7': {} + '@types/node@22.2.0': + dependencies: + undici-types: 6.13.0 + '@types/pug@2.0.10': {} + '@types/readdir-glob@1.1.5': + dependencies: + '@types/node': 22.2.0 + + '@types/semver@7.5.8': {} + '@types/webextension-polyfill@0.10.7': {} '@typescript-eslint/eslint-plugin@8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4)': @@ -2054,6 +2353,57 @@ snapshots: '@typescript-eslint/types': 8.0.1 eslint-visitor-keys: 3.4.3 + '@vitest/coverage-v8@2.0.5(vitest@2.0.5(@types/node@22.2.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.6 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.11 + magicast: 0.3.4 + std-env: 3.7.0 + test-exclude: 7.0.1 + tinyrainbow: 1.2.0 + vitest: 2.0.5(@types/node@22.2.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.0.5': + dependencies: + '@vitest/utils': 2.0.5 + pathe: 1.1.2 + + '@vitest/snapshot@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + magic-string: 0.30.11 + pathe: 1.1.2 + + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.0 + + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.1.1 + tinyrainbow: 1.2.0 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -2120,6 +2470,8 @@ snapshots: array-union@2.1.0: {} + assertion-error@2.0.1: {} + async@3.2.5: {} axobject-query@4.1.0: {} @@ -2157,13 +2509,25 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + cac@6.7.14: {} + callsites@3.1.0: {} + chai@5.1.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -2240,6 +2604,8 @@ snapshots: dependencies: ms: 2.1.2 + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -2401,6 +2767,18 @@ snapshots: events@3.3.0: {} + execa@8.0.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} @@ -2451,6 +2829,10 @@ snapshots: fsevents@2.3.3: optional: true + get-func-name@2.0.2: {} + + get-stream@8.0.1: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2467,6 +2849,15 @@ snapshots: minipass: 7.0.4 path-scurry: 1.10.1 + glob@10.4.5: + dependencies: + foreground-child: 3.1.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -2495,6 +2886,8 @@ snapshots: has-flag@4.0.0: {} + html-escaper@2.0.2: {} + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -2502,6 +2895,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@5.0.0: {} + ieee754@1.2.1: {} ignore@5.3.1: {} @@ -2542,16 +2937,45 @@ snapshots: is-stream@2.0.1: {} + is-stream@3.0.0: {} + isarray@1.0.0: {} isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.3.6 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jackspeak@2.3.6: dependencies: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -2589,10 +3013,16 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} + loupe@3.1.1: + dependencies: + get-func-name: 2.0.2 + lru-cache@10.2.0: {} magic-string@0.30.10: @@ -2603,10 +3033,22 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magicast@0.3.4: + dependencies: + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 + source-map-js: 1.2.0 + + make-dir@4.0.0: + dependencies: + semver: 7.6.3 + mdn-data@2.0.30: {} meow@12.1.1: {} + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.7: @@ -2614,6 +3056,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mimic-fn@4.0.0: {} + min-indent@1.0.1: {} minimatch@3.1.2: @@ -2632,6 +3076,8 @@ snapshots: minipass@7.0.4: {} + minipass@7.1.2: {} + mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -2650,10 +3096,18 @@ snapshots: normalize-path@3.0.0: {} + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + once@1.4.0: dependencies: wrappy: 1.0.2 + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2671,6 +3125,8 @@ snapshots: dependencies: p-limit: 3.1.0 + package-json-from-dist@1.0.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -2681,13 +3137,24 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-scurry@1.10.1: dependencies: lru-cache: 10.2.0 minipass: 7.0.4 + path-scurry@1.11.1: + dependencies: + lru-cache: 10.2.0 + minipass: 7.1.2 + path-type@4.0.0: {} + pathe@1.1.2: {} + + pathval@2.0.0: {} + periscopic@3.1.0: dependencies: '@types/estree': 1.0.5 @@ -2848,6 +3315,8 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} slash@3.0.0: {} @@ -2861,6 +3330,10 @@ snapshots: source-map-js@1.2.0: {} + stackback@0.0.2: {} + + std-env@3.7.0: {} + streamx@2.16.1: dependencies: fast-fifo: 1.3.2 @@ -2896,6 +3369,8 @@ snapshots: dependencies: ansi-regex: 6.0.1 + strip-final-newline@3.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -2984,8 +3459,24 @@ snapshots: fast-fifo: 1.3.2 streamx: 2.16.1 + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 + text-table@0.2.0: {} + tinybench@2.9.0: {} + + tinypool@1.0.0: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.0: {} + + to-fast-properties@2.0.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -3015,23 +3506,77 @@ snapshots: typescript@5.5.4: {} + undici-types@6.13.0: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 util-deprecate@1.0.2: {} - vite@5.4.0: + vite-node@2.0.5(@types/node@22.2.0): + dependencies: + cac: 6.7.14 + debug: 4.3.6 + pathe: 1.1.2 + tinyrainbow: 1.2.0 + vite: 5.4.0(@types/node@22.2.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.0(@types/node@22.2.0): dependencies: esbuild: 0.21.5 postcss: 8.4.41 rollup: 4.20.0 optionalDependencies: + '@types/node': 22.2.0 fsevents: 2.3.3 - vitefu@0.2.5(vite@5.4.0): + vitefu@0.2.5(vite@5.4.0(@types/node@22.2.0)): optionalDependencies: - vite: 5.4.0 + vite: 5.4.0(@types/node@22.2.0) + + vitest@2.0.5(@types/node@22.2.0): + dependencies: + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.5 + '@vitest/pretty-format': 2.0.5 + '@vitest/runner': 2.0.5 + '@vitest/snapshot': 2.0.5 + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + debug: 4.3.6 + execa: 8.0.1 + magic-string: 0.30.11 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinypool: 1.0.0 + tinyrainbow: 1.2.0 + vite: 5.4.0(@types/node@22.2.0) + vite-node: 2.0.5(@types/node@22.2.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.2.0 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser webextension-polyfill@0.12.0: {} @@ -3046,6 +3591,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: diff --git a/src/__mocks__/chrome/storage.ts b/src/__mocks__/chrome/storage.ts index 6792c82..72ecc28 100644 --- a/src/__mocks__/chrome/storage.ts +++ b/src/__mocks__/chrome/storage.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-explicit-any, no-console */ globalThis.chrome = globalThis.chrome || {}; globalThis.chrome.storage = globalThis.chrome.storage || {}; diff --git a/src/background/sw.ts b/src/background/sw.ts index 3f6a939..665282a 100644 --- a/src/background/sw.ts +++ b/src/background/sw.ts @@ -5,7 +5,12 @@ import { openPinnedTabs, closePinnedTabs, } from "../libs/controllers/_open-tabs"; -import { getUrlsToPin } from "../libs/models/_pinned-tabs"; +import { getUrlsToPin } from "../libs/models/_pinned-tabs-browser-storage"; +import { sleep } from "../libs/utils/_sleep"; + +const RETRY_SLEEP_MS = 100; +// After 5 minutes, give up. +const MAX_RETRIES = 5 * 60 * 1000; browser.action.onClicked.addListener(async (_tab) => { logger.log("Extension icon was clicked, loading pinned tabs..."); @@ -22,3 +27,25 @@ browser.action.onClicked.addListener(async (_tab) => { await openPinnedTabs(window.id); } }); + +browser.windows.onCreated.addListener(async (window) => { + if (window.type === "normal" && window.id) { + const windowID = window.id; + for (let i = 0; i <= MAX_RETRIES; i += RETRY_SLEEP_MS) { + try { + await closePinnedTabs(windowID); + await openPinnedTabs(windowID); + break; + } catch (err) { + // Sometimes the browser will throw if we try to open tabs too + // quickly. + logger.debug("Failed to open tabs, retrying...", err); + } + await sleep(RETRY_SLEEP_MS); + const windowToManage = await browser.windows.get(windowID); + if (windowToManage === undefined) { + break; + } + } + } +}); diff --git a/src/frontend/apps/options/Options.svelte b/src/frontend/apps/options/Options.svelte index 0368bf8..13e22b1 100644 --- a/src/frontend/apps/options/Options.svelte +++ b/src/frontend/apps/options/Options.svelte @@ -1,80 +1,86 @@

Options

- {#if pendingChanges} - - {/if} +
-
+
+

Using Pin-It

Edit the list of URLs below to setup which tabs you'd like to be pinned in your browser. Please make sure they are valid URLs, starting with http:// or https://.

-

Once set-up, just click the extension icon and the tabs will open.

+ +

+ Once set-up, just click the extension icon and the tabs will open, Learn more here. +

-
-
Pinned Tabs
-
- {#each urls as url, i (i)} - - {/each} - -
-
+ + +
+ + +
+ + diff --git a/src/frontend/apps/options/components/AutoOpenTabs.svelte b/src/frontend/apps/options/components/AutoOpenTabs.svelte new file mode 100644 index 0000000..30f99f5 --- /dev/null +++ b/src/frontend/apps/options/components/AutoOpenTabs.svelte @@ -0,0 +1,28 @@ + + + + + + + diff --git a/src/frontend/apps/options/components/LayoutOptionsSection.svelte b/src/frontend/apps/options/components/LayoutOptionsSection.svelte new file mode 100644 index 0000000..65af922 --- /dev/null +++ b/src/frontend/apps/options/components/LayoutOptionsSection.svelte @@ -0,0 +1,35 @@ + + +
+
+ {title}{#if isExperimental} +  Experimental + {/if} +
+
+
+ + diff --git a/src/frontend/apps/options/components/PinnedTabs.svelte b/src/frontend/apps/options/components/PinnedTabs.svelte new file mode 100644 index 0000000..4ab17ea --- /dev/null +++ b/src/frontend/apps/options/components/PinnedTabs.svelte @@ -0,0 +1,51 @@ + + + +
+ {#each urls as url, i (i)} + + {/each} + +
+
+ + diff --git a/src/frontend/apps/options/options.css b/src/frontend/apps/options/options.css index 35bb2d5..53d28eb 100644 --- a/src/frontend/apps/options/options.css +++ b/src/frontend/apps/options/options.css @@ -1,4 +1,4 @@ -@import '../../common/styles/_reset.css'; +@import "../../common/styles/_reset.css"; #app { display: flex; @@ -7,43 +7,8 @@ align-items: center; } -.l-settings { - display: grid; - grid-template-rows: auto auto auto 1fr auto; - width: 100%; - max-width: 680px; - flex: 1; - padding: var(--p-8); - container-type: inline-size; -} - -.l-settings-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.l-settings-section { - display: grid; - grid-template-columns: 1fr; - gap: var(--p-8); - - margin-block: var(--p-8); - margin-bottom: var(--p-16); -} - -.l-settings-section__title { - font-weight: 600; -} - -.l-settings-section__body-list { - display: inline-flex; - flex-direction: column; - gap: var(--p-4); -} - -.l-settings-section__body-list button { - align-self: center; +a { + color: var(--highlight); } .l-settings-bmc { @@ -51,12 +16,7 @@ } @container (width > 440px) { - .l-settings-section { - grid-template-columns: auto 1fr; - } - .l-settings-bmc { text-align: right; } - } diff --git a/src/frontend/components/checkbox/Checkbox.svelte b/src/frontend/components/checkbox/Checkbox.svelte new file mode 100644 index 0000000..3a64273 --- /dev/null +++ b/src/frontend/components/checkbox/Checkbox.svelte @@ -0,0 +1,87 @@ + + +
+
+ +
+ + +
+ + diff --git a/src/frontend/components/loader/Loader.svelte b/src/frontend/components/loader/Loader.svelte index c37c39b..d30f2ad 100644 --- a/src/frontend/components/loader/Loader.svelte +++ b/src/frontend/components/loader/Loader.svelte @@ -1,3 +1,42 @@ -
Loading
+ - +
Loading
+ + diff --git a/src/frontend/components/loader/c-loader.css b/src/frontend/components/loader/c-loader.css deleted file mode 100644 index 6be6640..0000000 --- a/src/frontend/components/loader/c-loader.css +++ /dev/null @@ -1,29 +0,0 @@ -.c-loader, -.c-loader:after { - border-radius: 50%; - width: var(--p-8); - aspect-ratio: 1; - overflow: hidden; -} - -.c-loader { - font-size: var(--p-4); - position: relative; - text-indent: -9999em; - border-width: var(--p-2); - border-style: solid; - border-color: var(--primary-grey); - border-left-color: var(--primary-light); - transform: translateZ(0); - animation: loaderKeyFrames 1.1s infinite linear; -} - -@keyframes loaderKeyFrames { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} diff --git a/src/libs/controllers/_open-tabs.ts b/src/libs/controllers/_open-tabs.ts index f9a2f50..92c0b72 100644 --- a/src/libs/controllers/_open-tabs.ts +++ b/src/libs/controllers/_open-tabs.ts @@ -1,6 +1,6 @@ import * as browser from "webextension-polyfill"; import { logger } from "../utils/_logger"; -import { getUrlsToPin } from "../../../src/libs/models/_pinned-tabs"; +import { getUrlsToPin } from "../models/_pinned-tabs-browser-storage"; /** * @param {number} windowID diff --git a/src/libs/models/_auto-open-tabs-browser-storage.ts b/src/libs/models/_auto-open-tabs-browser-storage.ts new file mode 100644 index 0000000..97ad9d5 --- /dev/null +++ b/src/libs/models/_auto-open-tabs-browser-storage.ts @@ -0,0 +1,24 @@ +import * as browser from "webextension-polyfill"; + +export const AUTO_OPEN_STORAGE_KEY = "auto-open-tabs"; + +export async function getAutoOpenTabs(): Promise { + const result = await browser.storage.sync.get(AUTO_OPEN_STORAGE_KEY); + if (result[AUTO_OPEN_STORAGE_KEY]) { + return result[AUTO_OPEN_STORAGE_KEY]; + } + + return { + autoOpenTabsNewWindow: false, + }; +} + +export async function setAutoOpenTabs(autoOpen: AutoOpenTabs): Promise { + await browser.storage.sync.set({ + [AUTO_OPEN_STORAGE_KEY]: autoOpen, + }); +} + +interface AutoOpenTabs { + autoOpenTabsNewWindow: boolean; +} diff --git a/src/libs/models/_auto-open-tabs-store.ts b/src/libs/models/_auto-open-tabs-store.ts new file mode 100644 index 0000000..e1ec579 --- /dev/null +++ b/src/libs/models/_auto-open-tabs-store.ts @@ -0,0 +1,74 @@ +import { + type Readable, + type Subscriber, + type Unsubscriber, +} from "svelte/store"; +import debounce, { type DebouncedFunc } from "lodash-es/debounce"; +import { + getAutoOpenTabs, + setAutoOpenTabs, +} from "./_auto-open-tabs-browser-storage"; + +export const PINNED_TAB_STORE_ID = "pinned-tabs"; + +export class AutoOpenTabsStore implements Readable { + private _subscribers = new Set>(); + private _autoOpenTabsNewWindow: boolean = false; + private _debounceCallCount = 0; + private _pendingChanges: boolean = false; + private _debouncedAutoOpen: DebouncedFunc<() => void>; + + constructor() { + this._debouncedAutoOpen = debounce(async () => { + const thisCallCount = this._debounceCallCount; + await setAutoOpenTabs({ + autoOpenTabsNewWindow: this._autoOpenTabsNewWindow, + }); + this._pendingChanges = this._debounceCallCount !== thisCallCount; + this.notifySubscribers(); + }, 1000); + this.initialize(); + } + + // Get the current state of this store + private get state(): AutoOpenTabs { + return { + autoOpenTabsNewWindow: this._autoOpenTabsNewWindow, + pendingChanges: this._pendingChanges, + }; + } + + // Initialize the values of this store + private async initialize() { + const opts = await getAutoOpenTabs(); + this._autoOpenTabsNewWindow = opts.autoOpenTabsNewWindow; + this.notifySubscribers(); + } + + // Subscribe to this store (Part of Readable interface) + subscribe(sub: Subscriber): Unsubscriber { + this._subscribers.add(sub); + sub(this.state); + return () => { + this._subscribers.delete(sub); + }; + } + + // Notify subscribers of a change in state + private notifySubscribers = () => { + this._subscribers.forEach((sub) => sub(this.state)); + }; + + setAutoOpenTabsNewWindow(open: boolean) { + this._autoOpenTabsNewWindow = open; + this._pendingChanges = true; + this.notifySubscribers(); + this._debounceCallCount++; + this._debouncedAutoOpen(); + } +} + +interface AutoOpenTabs { + autoOpenTabsNewWindow: boolean; + pendingChanges: boolean; +} diff --git a/src/libs/models/_debounce-work.ts b/src/libs/models/_debounce-work.ts deleted file mode 100644 index 074911b..0000000 --- a/src/libs/models/_debounce-work.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { debounce } from "lodash"; - -export class DebounceWork { - private debouncedFn: () => void; - private pendingCalls: number = 0; - private chain: Promise = Promise.resolve(); - - constructor(public fn: () => Promise) { - this.debouncedFn = debounce(() => { - // Reset pending calls - this.pendingCalls = 0; - // Append to queue of work - this.chain = this.chain.then(() => fn()); - }, 1000); - } - - run() { - this.pendingCalls++; - this.debouncedFn(); - } - - isComplete() { - return this.pendingCalls === 0; - } -} diff --git a/src/libs/models/_pinned-tabs-browser-storage.test.ts b/src/libs/models/_pinned-tabs-browser-storage.test.ts new file mode 100644 index 0000000..e5f5732 --- /dev/null +++ b/src/libs/models/_pinned-tabs-browser-storage.test.ts @@ -0,0 +1,77 @@ +import { expect, describe, test, beforeEach, afterEach, vi } from "vitest"; +import { getUrlsToPin, setUrlsToPin } from "./_pinned-tabs-browser-storage"; +import { storage } from "webextension-polyfill"; + +vi.mock("webextension-polyfill", () => { + return { + storage: { + sync: { + get: vi.fn().mockResolvedValue({}), + set: vi.fn().mockResolvedValue(undefined), + }, + }, + }; +}); + +beforeEach(() => {}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe("getUrlsToPin", () => { + test("returns empty array for new storage", async () => { + const urls = await getUrlsToPin(); + expect(urls).toEqual([]); + }); + + test("returns empty array from storage", async () => { + storage.sync.get = vi.fn().mockResolvedValue({ + "pinned-tabs": [], + }); + + const urls = await getUrlsToPin(); + expect(urls).toEqual([]); + }); + + test("returns single url from storage", async () => { + storage.sync.get = vi.fn().mockResolvedValue({ + "pinned-tabs": ["https://www.gaunt.dev"], + }); + + const urls = await getUrlsToPin(); + expect(urls).toEqual(["https://www.gaunt.dev"]); + }); + + test("returns multiple urls from storage", async () => { + storage.sync.get = vi.fn().mockResolvedValue({ + "pinned-tabs": ["https://www.gaunt.dev", "http://www.gaunt.dev"], + }); + + const urls = await getUrlsToPin(); + expect(urls).toEqual(["https://www.gaunt.dev", "http://www.gaunt.dev"]); + }); +}); + +describe("setUrlsToPin", () => { + test("saves empty array", async () => { + await setUrlsToPin([]); + expect(storage.sync.set).toHaveBeenCalledWith({ + "pinned-tabs": [], + }); + }); + + test("saves empty array for empty strings and invalid urls", async () => { + await setUrlsToPin(["", "this is not a URL"]); + expect(storage.sync.set).toHaveBeenCalledWith({ + "pinned-tabs": [], + }); + }); + + test("saves valid and normalized urls", async () => { + await setUrlsToPin(["http://www.gaunt.dev", "https://gaunt.dev/projects"]); + expect(storage.sync.set).toHaveBeenCalledWith({ + "pinned-tabs": ["http://www.gaunt.dev/", "https://gaunt.dev/projects"], + }); + }); +}); diff --git a/src/libs/models/_pinned-tabs.ts b/src/libs/models/_pinned-tabs-browser-storage.ts similarity index 76% rename from src/libs/models/_pinned-tabs.ts rename to src/libs/models/_pinned-tabs-browser-storage.ts index 12538d3..beeb256 100644 --- a/src/libs/models/_pinned-tabs.ts +++ b/src/libs/models/_pinned-tabs-browser-storage.ts @@ -1,9 +1,9 @@ -import * as browser from "webextension-polyfill"; +import { storage } from "webextension-polyfill"; export const URLS_TO_PIN_STORAGE_KEY = "pinned-tabs"; export async function getUrlsToPin(): Promise { - const result = await browser.storage.sync.get(URLS_TO_PIN_STORAGE_KEY); + const result = await storage.sync.get(URLS_TO_PIN_STORAGE_KEY); if (result[URLS_TO_PIN_STORAGE_KEY]) { return result[URLS_TO_PIN_STORAGE_KEY]; } @@ -21,7 +21,7 @@ export async function setUrlsToPin(urls: string[]): Promise { } }) .filter((u) => u.trim().length > 0); - await browser.storage.sync.set({ + await storage.sync.set({ [URLS_TO_PIN_STORAGE_KEY]: urls, }); } diff --git a/src/libs/models/_pinned-tabs-store.ts b/src/libs/models/_pinned-tabs-store.ts new file mode 100644 index 0000000..58b97c0 --- /dev/null +++ b/src/libs/models/_pinned-tabs-store.ts @@ -0,0 +1,69 @@ +import { + type Readable, + type Subscriber, + type Unsubscriber, +} from "svelte/store"; +import { getUrlsToPin, setUrlsToPin } from "./_pinned-tabs-browser-storage"; +import debounce, { type DebouncedFunc } from "lodash-es/debounce"; + +export class PinnedTabsStore implements Readable { + private _subscribers = new Set>(); + private _urls: string[] = []; + private _debounceCallCount = 0; + private _pendingChanges: boolean = false; + private _debouncedSetURLs: DebouncedFunc<() => void>; + + constructor() { + this._debouncedSetURLs = debounce(async () => { + const thisCallCount = this._debounceCallCount; + await setUrlsToPin(this._urls); + this._pendingChanges = this._debounceCallCount !== thisCallCount; + this.notifySubscribers(); + }, 1000); + this.initialize(); + } + + // Get the current state of this store + private get state(): PinnedTabs { + return { + urls: this._urls, + pendingChanges: this._pendingChanges, + }; + } + + // Initialize the values of this store + private async initialize() { + this._urls = await getUrlsToPin(); + if (this._urls.length === 0) { + this._urls = [""]; + } + this.notifySubscribers(); + } + + // Subscribe to this store (Part of Readable interface) + subscribe(sub: Subscriber): Unsubscriber { + this._subscribers.add(sub); + sub(this.state); + return () => { + this._subscribers.delete(sub); + }; + } + + // Notify subscribers of a change in state + private notifySubscribers = () => { + this._subscribers.forEach((sub) => sub(this.state)); + }; + + saveURLs(urls: string[]) { + this._urls = urls; + this._pendingChanges = true; + this.notifySubscribers(); + this._debounceCallCount++; + this._debouncedSetURLs(); + } +} + +interface PinnedTabs { + urls: string[]; + pendingChanges: boolean; +} diff --git a/src/libs/utils/_sleep.test.ts b/src/libs/utils/_sleep.test.ts new file mode 100644 index 0000000..7c11e80 --- /dev/null +++ b/src/libs/utils/_sleep.test.ts @@ -0,0 +1,16 @@ +import { expect, test, beforeEach, afterEach, vi } from "vitest"; +import { sleep } from "./_sleep"; + +beforeEach(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +test("sleep by set amount", () => { + const promise = sleep(100); + vi.advanceTimersByTime(100 + 1); + expect(promise).resolves.toBeUndefined(); +}); diff --git a/src/libs/utils/_sleep.ts b/src/libs/utils/_sleep.ts new file mode 100644 index 0000000..0f532df --- /dev/null +++ b/src/libs/utils/_sleep.ts @@ -0,0 +1,3 @@ +export function sleep(delay: number): Promise { + return new Promise((resolve) => setTimeout(resolve, delay)); +} diff --git a/vite.config.ts b/vite.config.ts index be3b485..ffcce93 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -59,7 +59,7 @@ async function bundleExtension() { const output = createWriteStream(zipPath); // eslint-disable-next-line new-cap - const archive = new archiver("zip", { + const archive = archiver("zip", { zlib: { // Sets the compression level level: 9,