diff options
41 files changed, 545 insertions, 596 deletions
diff --git a/build/readme-parser.ts b/build/readme-parser.ts index 5e6e36de..45db66c8 100644 --- a/build/readme-parser.ts +++ b/build/readme-parser.ts @@ -1,4 +1,4 @@ -/// <reference types="../source/globals" /> +/// <reference types="../source/globals.js" /> import regexJoin from 'regex-join'; import {readFileSync} from 'node:fs'; diff --git a/build/verify-test-urls.sh b/build/verify-test-urls.sh index dfbfd65b..3433ec6e 100755 --- a/build/verify-test-urls.sh +++ b/build/verify-test-urls.sh @@ -48,12 +48,11 @@ for FILE in "$@"; do fi done -echo ::group::GITHUB ANNOTATIONS -cat "$ANNOTATIONS" -echo ::endgroup::: - # Don't fail PRs that edit a large number of files if [ "$ERRORS" -ge 1 ] && [ "$ERRORS" -le 3 ]; then + echo ::group::GITHUB ANNOTATIONS + cat $ANNOTATIONS + echo ::endgroup::: echo echo VERIFICATION FAILED, "Test URLs" MISSING exit $ERRORS diff --git a/package-lock.json b/package-lock.json index c340ef1d..c1d74ae7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "dom-chef": "^5.1.0", "dom-loaded": "^3.0.0", "doma": "^3.0.2", - "element-ready": "^6.2.1", + "element-ready": "^6.2.2", "filter-altered-clicks": "^2.0.0", "fit-textarea": "^2.0.0", "flat-zip": "^1.0.1", @@ -34,7 +34,7 @@ "pretty-bytes": "^6.1.0", "push-form": "^1.0.1", "regex-join": "^2.0.0", - "select-dom": "^7.1.1", + "select-dom": "^8.0.0", "shorten-repo-url": "^3.0.0", "strip-indent": "^4.0.0", "text-field-edit": "^3.1.9001", @@ -46,7 +46,7 @@ "webext-domain-permission-toggle": "^4.0.1", "webext-dynamic-content-scripts": "^9.2.0", "webext-options-sync-per-domain": "^4.0.0", - "webext-storage-cache": "^6.0.0-2", + "webext-storage-cache": "^6.0.0-5", "webextension-polyfill-global": "^0.10.1-1", "zip-text-nodes": "^1.0.0" }, @@ -76,10 +76,10 @@ "terser-webpack-plugin": "^5.3.6", "tsx": "^3.12.7", "type-fest": "^3.5.7", - "typed-query-selector": "^2.9.1", + "typed-query-selector": "^2.11.0", "typescript": "^4.9.5", "vitest": "^0.28.4", - "webpack": "^5.82.0", + "webpack": "^5.88.1", "webpack-cli": "^5.0.1", "xo": "^0.53.1" }, @@ -1643,9 +1643,9 @@ } }, "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "dev": true, "peerDependencies": { "acorn": "^8" @@ -2911,9 +2911,9 @@ "dev": true }, "node_modules/element-ready": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/element-ready/-/element-ready-6.2.1.tgz", - "integrity": "sha512-C+3oy1RwVLO1GpOHhFerYuLp7PQ3btochVt/A5X4HEZOvIZ8LfWfguMo1boIREyV++sjyi1Zcb7QsvSjic7dpQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/element-ready/-/element-ready-6.2.2.tgz", + "integrity": "sha512-aERDpVjwsI6uSuOOwEaayPuHK6NAW3IuH11gaaquwIcwGblb9NseughqtB5yqAOsw2C65vApDecfREJZXJ8QGQ==", "dependencies": { "deferred-async-iterator": "^2.0.0", "many-keys-map": "^1.0.3", @@ -2955,9 +2955,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", - "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -8516,11 +8516,17 @@ "dev": true }, "node_modules/select-dom": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/select-dom/-/select-dom-7.1.1.tgz", - "integrity": "sha512-W4052AVb0TkSU8MgiNLiVio4WqCLpDSWPYz5RI6C5MoXfjo6b8p0v/Ld2iK9F/FO2FUqn6N0jFlBGyl2z5dzbA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/select-dom/-/select-dom-8.0.0.tgz", + "integrity": "sha512-U0/vlppYeRVFNv3N78I9Kj+wNfm5szZGCsFsoF8rmAOhXF9kXKdtJJeXYS/ghubvRblqjJDyRI/1aOoiJgw+0g==", "dependencies": { - "typed-query-selector": "^2.4.1" + "typed-query-selector": "^2.11.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/fregante" } }, "node_modules/semver": { @@ -9563,10 +9569,9 @@ } }, "node_modules/type-fest": { - "version": "3.5.7", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.7.tgz", - "integrity": "sha512-6J4bYzb4sdkcLBty4XW7F18VPI66M4boXNE+CY40532oq2OJe6AVMB5NmjOp6skt/jw5mRjz/hLRpuglz0U+FA==", - "dev": true, + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.11.1.tgz", + "integrity": "sha512-aCuRNRERRVh33lgQaJRlUxZqzfhzwTrsE98Mc3o3VXqmiaQdHacgUtJ0esp+7MvZ92qhtzKPeusaX6vIEcoreA==", "engines": { "node": ">=14.16" }, @@ -9589,9 +9594,9 @@ } }, "node_modules/typed-query-selector": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.10.1.tgz", - "integrity": "sha512-VyNeetDghQngbLWOVyxUFB95nZF3z3QIrvdsWmuDwwYbePEup9hjZbNT1IK5YP3t8LqicnYNnDpQPW3vq7iCTQ==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.11.0.tgz", + "integrity": "sha512-qBs4sfmnLlPOyo2oSdvHbIFHe2CPgU54/1UGfSNceb7LARpIEVxUaeRX0Doje6oKpuySS2stqy90R3YrynR8Kg==" }, "node_modules/typescript": { "version": "4.9.5", @@ -10062,13 +10067,17 @@ } }, "node_modules/webext-storage-cache": { - "version": "6.0.0-2", - "resolved": "https://registry.npmjs.org/webext-storage-cache/-/webext-storage-cache-6.0.0-2.tgz", - "integrity": "sha512-fez9KD5dhx7KNi9CumHlZnLcTkn1i6h1hc+fUHKC4w8s2PnEINJOXR+jHeuu/EyCkZrW7dpzahDWLKjX9+tPZw==", + "version": "6.0.0-5", + "resolved": "https://registry.npmjs.org/webext-storage-cache/-/webext-storage-cache-6.0.0-5.tgz", + "integrity": "sha512-uqnJz/PP6X6/Nbfag9CT7VOh1EKtE1JHmwINX6OJFSvmZPKJQR5U1hn82BBhWY6DtxOvKjZ3j+/kYCYfgsCZ9g==", "dependencies": { "@sindresorhus/to-milliseconds": "^2.0.0", - "webext-detect-page": "^4.0.1", - "webext-polyfill-kinda": "^1.0.0" + "type-fest": "^3.11.0", + "webext-detect-page": "^4.1.0", + "webext-polyfill-kinda": "^1.0.2" + }, + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/fregante" @@ -10110,9 +10119,9 @@ } }, "node_modules/webpack": { - "version": "5.82.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.0.tgz", - "integrity": "sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg==", + "version": "5.88.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz", + "integrity": "sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -10121,10 +10130,10 @@ "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.13.0", + "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -10134,7 +10143,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.7", "watchpack": "^2.4.0", @@ -10256,9 +10265,9 @@ } }, "node_modules/webpack/node_modules/schema-utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", - "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.8", @@ -12548,9 +12557,9 @@ } }, "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "dev": true, "requires": {} }, @@ -13485,9 +13494,9 @@ "dev": true }, "element-ready": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/element-ready/-/element-ready-6.2.1.tgz", - "integrity": "sha512-C+3oy1RwVLO1GpOHhFerYuLp7PQ3btochVt/A5X4HEZOvIZ8LfWfguMo1boIREyV++sjyi1Zcb7QsvSjic7dpQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/element-ready/-/element-ready-6.2.2.tgz", + "integrity": "sha512-aERDpVjwsI6uSuOOwEaayPuHK6NAW3IuH11gaaquwIcwGblb9NseughqtB5yqAOsw2C65vApDecfREJZXJ8QGQ==", "requires": { "deferred-async-iterator": "^2.0.0", "many-keys-map": "^1.0.3", @@ -13517,9 +13526,9 @@ } }, "enhanced-resolve": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", - "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -17419,11 +17428,11 @@ } }, "select-dom": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/select-dom/-/select-dom-7.1.1.tgz", - "integrity": "sha512-W4052AVb0TkSU8MgiNLiVio4WqCLpDSWPYz5RI6C5MoXfjo6b8p0v/Ld2iK9F/FO2FUqn6N0jFlBGyl2z5dzbA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/select-dom/-/select-dom-8.0.0.tgz", + "integrity": "sha512-U0/vlppYeRVFNv3N78I9Kj+wNfm5szZGCsFsoF8rmAOhXF9kXKdtJJeXYS/ghubvRblqjJDyRI/1aOoiJgw+0g==", "requires": { - "typed-query-selector": "^2.4.1" + "typed-query-selector": "^2.11.0" } }, "semver": { @@ -18201,10 +18210,9 @@ "dev": true }, "type-fest": { - "version": "3.5.7", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.7.tgz", - "integrity": "sha512-6J4bYzb4sdkcLBty4XW7F18VPI66M4boXNE+CY40532oq2OJe6AVMB5NmjOp6skt/jw5mRjz/hLRpuglz0U+FA==", - "dev": true + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.11.1.tgz", + "integrity": "sha512-aCuRNRERRVh33lgQaJRlUxZqzfhzwTrsE98Mc3o3VXqmiaQdHacgUtJ0esp+7MvZ92qhtzKPeusaX6vIEcoreA==" }, "typed-array-length": { "version": "1.0.4", @@ -18218,9 +18226,9 @@ } }, "typed-query-selector": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.10.1.tgz", - "integrity": "sha512-VyNeetDghQngbLWOVyxUFB95nZF3z3QIrvdsWmuDwwYbePEup9hjZbNT1IK5YP3t8LqicnYNnDpQPW3vq7iCTQ==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.11.0.tgz", + "integrity": "sha512-qBs4sfmnLlPOyo2oSdvHbIFHe2CPgU54/1UGfSNceb7LARpIEVxUaeRX0Doje6oKpuySS2stqy90R3YrynR8Kg==" }, "typescript": { "version": "4.9.5", @@ -18543,13 +18551,14 @@ "integrity": "sha512-rqQUKeBTOicej0tjDJWDQlOTnDcm9yYJTzgI+7rMdyYV4QHmYMRm+yjkcVgECkg/Wu9MboZ4lYeBPdp1Ep9WgQ==" }, "webext-storage-cache": { - "version": "6.0.0-2", - "resolved": "https://registry.npmjs.org/webext-storage-cache/-/webext-storage-cache-6.0.0-2.tgz", - "integrity": "sha512-fez9KD5dhx7KNi9CumHlZnLcTkn1i6h1hc+fUHKC4w8s2PnEINJOXR+jHeuu/EyCkZrW7dpzahDWLKjX9+tPZw==", + "version": "6.0.0-5", + "resolved": "https://registry.npmjs.org/webext-storage-cache/-/webext-storage-cache-6.0.0-5.tgz", + "integrity": "sha512-uqnJz/PP6X6/Nbfag9CT7VOh1EKtE1JHmwINX6OJFSvmZPKJQR5U1hn82BBhWY6DtxOvKjZ3j+/kYCYfgsCZ9g==", "requires": { "@sindresorhus/to-milliseconds": "^2.0.0", - "webext-detect-page": "^4.0.1", - "webext-polyfill-kinda": "^1.0.0" + "type-fest": "^3.11.0", + "webext-detect-page": "^4.1.0", + "webext-polyfill-kinda": "^1.0.2" } }, "webext-tools": { @@ -18582,9 +18591,9 @@ "dev": true }, "webpack": { - "version": "5.82.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.0.tgz", - "integrity": "sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg==", + "version": "5.88.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz", + "integrity": "sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", @@ -18593,10 +18602,10 @@ "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.13.0", + "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -18606,7 +18615,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.7", "watchpack": "^2.4.0", @@ -18630,9 +18639,9 @@ "dev": true }, "schema-utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", - "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "requires": { "@types/json-schema": "^7.0.8", diff --git a/package.json b/package.json index 1990b90e..441b1395 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint:js": "xo", "pack:safari": "xcodebuild -project 'safari/Refined GitHub.xcodeproj' -scheme 'Refined GitHub (macOS)'", "prepare:safari": "bash build/prepare-safari-release.sh", - "test": "run-p vitest lint:* build:* test:features", + "test": "run-p vitest lint:* build:* test:features --continue-on-error", "test:features": "tsx build/verify-features.ts", "watch": "run-p watch:* --continue-on-error", "watch:typescript": "tsc --noEmit --watch --preserveWatchOutput", @@ -52,7 +52,7 @@ "dom-chef": "^5.1.0", "dom-loaded": "^3.0.0", "doma": "^3.0.2", - "element-ready": "^6.2.1", + "element-ready": "^6.2.2", "filter-altered-clicks": "^2.0.0", "fit-textarea": "^2.0.0", "flat-zip": "^1.0.1", @@ -70,7 +70,7 @@ "pretty-bytes": "^6.1.0", "push-form": "^1.0.1", "regex-join": "^2.0.0", - "select-dom": "^7.1.1", + "select-dom": "^8.0.0", "shorten-repo-url": "^3.0.0", "strip-indent": "^4.0.0", "text-field-edit": "^3.1.9001", @@ -82,7 +82,7 @@ "webext-domain-permission-toggle": "^4.0.1", "webext-dynamic-content-scripts": "^9.2.0", "webext-options-sync-per-domain": "^4.0.0", - "webext-storage-cache": "^6.0.0-2", + "webext-storage-cache": "^6.0.0-5", "webextension-polyfill-global": "^0.10.1-1", "zip-text-nodes": "^1.0.0" }, @@ -112,10 +112,10 @@ "terser-webpack-plugin": "^5.3.6", "tsx": "^3.12.7", "type-fest": "^3.5.7", - "typed-query-selector": "^2.9.1", + "typed-query-selector": "^2.11.0", "typescript": "^4.9.5", "vitest": "^0.28.4", - "webpack": "^5.82.0", + "webpack": "^5.88.1", "webpack-cli": "^5.0.1", "xo": "^0.53.1" }, diff --git a/source/background.ts b/source/background.ts index fae088cd..209f722e 100644 --- a/source/background.ts +++ b/source/background.ts @@ -1,7 +1,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import {type Runtime} from 'webextension-polyfill'; import 'webext-dynamic-content-scripts'; -import cache from 'webext-storage-cache'; // Also needed to regularly clear the cache +import {globalCache} from 'webext-storage-cache'; // Also needed to regularly clear the cache import {isSafari} from 'webext-detect-page'; import {objectKeys} from 'ts-extras'; import addDomainPermissionToggle from 'webext-domain-permission-toggle'; @@ -89,11 +89,7 @@ browser.runtime.onInstalled.addListener(async ({reason}) => { }); } - // Hope that the feature was fixed in this version - await cache.delete('hotfixes:'); - await cache.delete('style-hotfixes:'); - if (isDevelopmentVersion()) { - await cache.clear(); + await globalCache.clear(); } }); diff --git a/source/feature-manager.tsx b/source/feature-manager.tsx index 0d951544..0855be0e 100644 --- a/source/feature-manager.tsx +++ b/source/feature-manager.tsx @@ -14,11 +14,10 @@ import {isFeaturePrivate} from './helpers/feature-utils.js'; import optionsStorage, {isFeatureDisabled, RGHOptions} from './options-storage.js'; import { applyStyleHotfixes, - getStyleHotfix, + styleHotfixes, getLocalHotfixesAsOptions, - getLocalStrings, - updateHotfixes, - updateLocalStrings, + preloadSyncLocalStrings, + brokenFeatures, _, } from './helpers/hotfix.js'; @@ -107,7 +106,7 @@ const globalReady = new Promise<RGHOptions>(async resolve => { optionsStorage.getAll(), getLocalHotfixesAsOptions(), bisectFeatures(), - getLocalStrings(), + preloadSyncLocalStrings(), ]); await waitFor(() => document.body); @@ -130,20 +129,18 @@ const globalReady = new Promise<RGHOptions>(async resolve => { document.documentElement.classList.add('refined-github'); - void getStyleHotfix(version).then(applyStyleHotfixes); + void styleHotfixes.get(version).then(applyStyleHotfixes); if (options.customCSS.trim().length > 0) { // Review #5857 and #5493 before making changes document.head.append(<style>{options.customCSS}</style>); } - void updateLocalStrings(); - if (bisectedFeatures) { Object.assign(options, bisectedFeatures); } else { // If features are remotely marked as "seriously breaking" by the maintainers, disable them without having to wait for proper updates to propagate #3529 - void updateHotfixes(version); + void brokenFeatures.get(); Object.assign(options, localHotfixes); } diff --git a/source/features/bugs-tab.tsx b/source/features/bugs-tab.tsx index 201bd1ff..86fb7e42 100644 --- a/source/features/bugs-tab.tsx +++ b/source/features/bugs-tab.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import {BugIcon} from '@primer/octicons-react'; import elementReady from 'element-ready'; @@ -7,16 +7,18 @@ import * as pageDetect from 'github-url-detection'; import features from '../feature-manager.js'; import api from '../github-helpers/api.js'; -import {cacheByRepo, getRepo} from '../github-helpers/index.js'; +import {cacheByRepo} from '../github-helpers/index.js'; import SearchQuery from '../github-helpers/search-query.js'; import abbreviateNumber from '../helpers/abbreviate-number.js'; import {highlightTab, unhighlightTab} from '../helpers/dom-utils.js'; import isBugLabel from '../github-helpers/bugs-label.js'; -const getBugLabelCacheKey = (): string => 'bugs-label:' + getRepo()!.nameWithOwner; -const getBugLabel = async (): Promise<string | undefined> => cache.get<string>(getBugLabelCacheKey()); +type Bugs = { + label: string; + count: number; +}; -async function countBugsWithUnknownLabel(): Promise<number> { +async function countBugs(): Promise<Bugs> { const {repository} = await api.v4(` repository() { labels(query: "bug", first: 10) { @@ -30,45 +32,32 @@ async function countBugsWithUnknownLabel(): Promise<number> { } `); - const label: AnyObject | undefined = repository.labels.nodes - .find((label: AnyObject) => isBugLabel(label.name)); - if (!label) { - return 0; + // Prefer native "bug" label + for (const label of repository.labels.nodes) { + if (label.name === 'bug') { + return {label: 'bug', count: label.issues.totalCount ?? 0}; + } } - void cache.set(getBugLabelCacheKey(), label.name ?? false); - return label.issues.totalCount ?? 0; -} - -async function countIssuesWithLabel(label: string): Promise<number> { - const {repository} = await api.v4(` - query bugIssueCount($owner: String!, $name: String!, $label: String!) { - repository(owner: $owner, name: $name) { - label(name: $label) { - issues(states: OPEN) { - totalCount - } - } - } + for (const label of repository.labels.nodes) { + if (isBugLabel(label.name)) { + return {label: label.name, count: label.issues.totalCount ?? 0}; } - `, {variables: {label}}); + } - return repository.label?.issues.totalCount ?? 0; + return {label: '', count: 0}; } -const countBugs = cache.function('bugs', async (): Promise<number> => { - const bugLabel = await getBugLabel(); - return bugLabel - ? countIssuesWithLabel(bugLabel) - : countBugsWithUnknownLabel(); -}, { +const bugs = new CachedFunction('bugs', { + updater: countBugs, maxAge: {minutes: 30}, staleWhileRevalidate: {days: 4}, cacheKey: cacheByRepo, }); async function getSearchQueryBugLabel(): Promise<string> { - return 'label:' + SearchQuery.escapeValue(await getBugLabel() ?? 'bug'); + const {label} = await bugs.getCached() ?? {}; + return 'label:' + SearchQuery.escapeValue(label ?? 'bug'); } async function isBugsListing(): Promise<boolean> { @@ -77,15 +66,18 @@ async function isBugsListing(): Promise<boolean> { async function addBugsTab(): Promise<void | false> { // Query API as early as possible, even if it's not necessary on archived repos - const countPromise = countBugs(); + const bugsPromise = bugs.get(); // On a label:bug listing: // - always show the tab, as soon as possible // - update the count later // On other pages: // - only show the tab if needed - if (!await isBugsListing() && await countPromise === 0) { - return false; + if (!await isBugsListing()) { + const {count} = await bugsPromise; + if (count === 0) { + return false; + } } const issuesTab = await elementReady('a.UnderlineNav-item[data-hotkey="g i"]', {waitForChildren: false}); @@ -130,7 +122,7 @@ async function addBugsTab(): Promise<void | false> { // Update bugs count try { - const bugCount = await countPromise; + const {count: bugCount} = await bugsPromise; bugsCounter.textContent = abbreviateNumber(bugCount); bugsCounter.title = bugCount > 999 ? String(bugCount) : ''; } catch (error) { @@ -151,13 +143,13 @@ async function removePinnedIssues(): Promise<void> { } async function updateBugsTagHighlighting(): Promise<void | false> { - if (await countBugs() === 0) { + const {count, label} = await bugs.get(); + if (count === 0) { return false; } - const bugLabel = await getBugLabel() ?? 'bug'; if ( - (pageDetect.isRepoTaxonomyIssueOrPRList() && location.href.endsWith('/labels/' + encodeURIComponent(bugLabel))) + (pageDetect.isRepoTaxonomyIssueOrPRList() && location.href.endsWith('/labels/' + encodeURIComponent(label))) || (pageDetect.isRepoIssueList() && await isBugsListing()) ) { void removePinnedIssues(); @@ -165,7 +157,7 @@ async function updateBugsTagHighlighting(): Promise<void | false> { return; } - if (pageDetect.isIssue() && await elementReady(`#partial-discussion-sidebar .IssueLabel[data-name="${bugLabel}"]`)) { + if (pageDetect.isIssue() && await elementReady(`#partial-discussion-sidebar .IssueLabel[data-name="${label}"]`)) { highlightBugsTab(); return; } diff --git a/source/features/clean-conversation-filters.tsx b/source/features/clean-conversation-filters.tsx index e921ef80..80c4b9b4 100644 --- a/source/features/clean-conversation-filters.tsx +++ b/source/features/clean-conversation-filters.tsx @@ -1,4 +1,4 @@ -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import elementReady from 'element-ready'; import * as pageDetect from 'github-url-detection'; @@ -7,9 +7,10 @@ import features from '../feature-manager.js'; import api, {expectTokenScope} from '../github-helpers/api.js'; import {cacheByRepo} from '../github-helpers/index.js'; -const hasAnyProjects = cache.function('has-projects', async (): Promise<boolean> => { - await expectTokenScope('read:project'); - const {repository, organization} = await api.v4(` +const hasAnyProjects = new CachedFunction('has-projects', { + async updater(): Promise<boolean> { + await expectTokenScope('read:project'); + const {repository, organization} = await api.v4(` query hasAnyProjects($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { projects { totalCount } @@ -21,14 +22,14 @@ const hasAnyProjects = cache.function('has-projects', async (): Promise<boolean> } } `, { - allowErrors: true, - }); + allowErrors: true, + }); - return Boolean(repository.projects.totalCount) + return Boolean(repository.projects.totalCount) || Boolean(repository.projectsV2.totalCount) || Boolean(organization?.projects?.totalCount) || Boolean(organization?.projectsV2?.totalCount); -}, { + }, maxAge: {days: 1}, staleWhileRevalidate: {days: 20}, cacheKey: cacheByRepo, @@ -58,7 +59,7 @@ async function hasProjects(): Promise<boolean> { return false; } - return hasAnyProjects(); + return hasAnyProjects.get(); } async function hideProjects(): Promise<void> { diff --git a/source/features/clean-repo-tabs.tsx b/source/features/clean-repo-tabs.tsx index 0fe52514..ab1b1b8d 100644 --- a/source/features/clean-repo-tabs.tsx +++ b/source/features/clean-repo-tabs.tsx @@ -1,4 +1,4 @@ -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import elementReady from 'element-ready'; import * as pageDetect from 'github-url-detection'; @@ -46,23 +46,25 @@ function onlyShowInDropdown(id: string): void { select('.UnderlineNav-actions ul')!.append(menuItem); } -const getWikiPageCount = cache.function('wiki-page-count', async (): Promise<number> => { - const dom = await fetchDom(buildRepoURL('wiki')); - const counter = dom.querySelector('#wiki-pages-box .Counter'); +const wikiPageCount = new CachedFunction('wiki-page-count', { + async updater(): Promise<number> { + const dom = await fetchDom(buildRepoURL('wiki')); + const counter = dom.querySelector('#wiki-pages-box .Counter'); - if (counter) { - return looseParseInt(counter); - } + if (counter) { + return looseParseInt(counter); + } - return dom.querySelectorAll('#wiki-content > .Box .Box-row').length; -}, { + return dom.querySelectorAll('#wiki-content > .Box .Box-row').length; + }, maxAge: {hours: 1}, staleWhileRevalidate: {days: 5}, cacheKey: cacheByRepo, }); -const getWorkflowsCount = cache.function('workflows-count', async (): Promise<number> => { - const {repository: {workflowFiles}} = await api.v4(` +const workflowCount = new CachedFunction('workflows-count', { + async updater(): Promise<number> { + const {repository: {workflowFiles}} = await api.v4(` repository() { workflowFiles: object(expression: "HEAD:.github/workflows") { ... on Tree { entries { oid } } @@ -70,8 +72,8 @@ const getWorkflowsCount = cache.function('workflows-count', async (): Promise<nu } `); - return workflowFiles?.entries.length ?? 0; -}, { + return workflowFiles?.entries.length ?? 0; + }, maxAge: {days: 1}, staleWhileRevalidate: {days: 10}, cacheKey: cacheByRepo, @@ -83,9 +85,9 @@ async function updateWikiTab(): Promise<void | false> { return false; } - const wikiPageCount = await getWikiPageCount(); - if (wikiPageCount > 0) { - setTabCounter(wikiTab, wikiPageCount); + const count = await wikiPageCount.get(); + if (count > 0) { + setTabCounter(wikiTab, count); } else { onlyShowInDropdown('wiki-tab'); } @@ -93,7 +95,7 @@ async function updateWikiTab(): Promise<void | false> { async function updateActionsTab(): Promise<void | false> { const actionsTab = await elementReady('[data-hotkey="g a"]'); - if (!actionsTab || mustKeepTab(actionsTab) || await getWorkflowsCount() > 0) { + if (!actionsTab || mustKeepTab(actionsTab) || await workflowCount.get() > 0) { return false; } diff --git a/source/features/closing-remarks.tsx b/source/features/closing-remarks.tsx index a38f12ad..e97fd418 100644 --- a/source/features/closing-remarks.tsx +++ b/source/features/closing-remarks.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import {TagIcon} from '@primer/octicons-react'; import * as pageDetect from 'github-url-detection'; @@ -12,20 +12,21 @@ import TimelineItem from '../github-helpers/timeline-item.js'; import attachElement from '../helpers/attach-element.js'; import {canEditEveryComment} from './quick-comment-edit.js'; import {buildRepoURL, getRepo, isRefinedGitHubRepo} from '../github-helpers/index.js'; -import {getReleaseCount} from './releases-tab.js'; +import {releasesCount} from './releases-tab.js'; import observe from '../helpers/selector-observer.js'; // TODO: Not an exact match; Moderators can edit comments but not create releases const canCreateRelease = canEditEveryComment; -const getFirstTag = cache.function('first-tag', async (commit: string): Promise<string | undefined> => { - const firstTag = await fetchDom( - buildRepoURL('branch_commits', commit), - 'ul.branches-tag-list li:last-child a', - ); +const firstTag = new CachedFunction('first-tag', { + async updater(commit: string): Promise<string | false> { + const firstTag = await fetchDom( + buildRepoURL('branch_commits', commit), + 'ul.branches-tag-list li:last-child a', + ); - return firstTag?.textContent ?? undefined; -}, { + return firstTag?.textContent ?? false; + }, cacheKey: ([commit]) => [getRepo()!.nameWithOwner, commit].join(':'), }); @@ -43,7 +44,7 @@ function createReleaseUrl(): string | undefined { async function init(signal: AbortSignal): Promise<void> { const mergeCommit = select(`.TimelineItem.js-details-container.Details a[href^="/${getRepo()!.nameWithOwner}/commit/" i] > code`)!.textContent!; - const tagName = await getFirstTag(mergeCommit); + const tagName = await firstTag.get(mergeCommit); if (tagName) { const tagUrl = buildRepoURL('releases/tag', tagName); @@ -90,7 +91,7 @@ function addExistingTagLinkFooter(tagName: string, tagUrl: string): void { } async function addReleaseBanner(text = 'Now you can release this change'): Promise<void> { - if (await getReleaseCount() === 0) { + if (await releasesCount.get(getRepo()!.nameWithOwner) === 0) { return; } diff --git a/source/features/github-actions-indicators.tsx b/source/features/github-actions-indicators.tsx index 7d2ffe8f..f90b7a70 100644 --- a/source/features/github-actions-indicators.tsx +++ b/source/features/github-actions-indicators.tsx @@ -1,4 +1,4 @@ -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import React from 'dom-chef'; import select from 'select-dom'; import {StopIcon, PlayIcon} from '@primer/octicons-react'; @@ -72,30 +72,31 @@ async function getFilesInWorkflowPath(): Promise<Record<string, string>> { return result; } -const getWorkflowsDetails = cache.function('workflows-details', async (): Promise<Record<string, Workflow & WorkflowDetails>> => { - const [workflows, workflowFiles] = await Promise.all([getWorkflows(), getFilesInWorkflowPath()]); +const workflowDetails = new CachedFunction('workflows-details', { + async updater(): Promise<Record<string, Workflow & WorkflowDetails>> { + const [workflows, workflowFiles] = await Promise.all([getWorkflows(), getFilesInWorkflowPath()]); - const details: Record<string, Workflow & WorkflowDetails> = {}; + const details: Record<string, Workflow & WorkflowDetails> = {}; - for (const workflow of workflows) { - const workflowYaml = workflowFiles[workflow.name]; + for (const workflow of workflows) { + const workflowYaml = workflowFiles[workflow.name]; - if (workflowYaml === undefined) { + if (workflowYaml === undefined) { // Cannot find workflow yaml; workflow removed. - continue; - } + continue; + } - const cron = /schedule[:\s-]+cron[:\s'"]+([^'"\n]+)/m.exec(workflowYaml); + const cron = /schedule[:\s-]+cron[:\s'"]+([^'"\n]+)/m.exec(workflowYaml); - details[workflow.name] = { - ...workflow, - schedule: cron?.[1], - manuallyDispatchable: workflowYaml.includes('workflow_dispatch:'), - }; - } + details[workflow.name] = { + ...workflow, + schedule: cron?.[1], + manuallyDispatchable: workflowYaml.includes('workflow_dispatch:'), + }; + } - return details; -}, { + return details; + }, maxAge: {days: 1}, staleWhileRevalidate: {days: 10}, cacheKey: cacheByRepo, @@ -108,7 +109,7 @@ async function addIndicators(workflowListItem: HTMLAnchorElement): Promise<void> } // Called in `init`, memoized - const workflows = await getWorkflowsDetails(); + const workflows = await workflowDetails.get(); const workflowName = workflowListItem.href.split('/').pop()!; const workflow = workflows[workflowName]; if (!workflow) { @@ -152,7 +153,7 @@ async function addIndicators(workflowListItem: HTMLAnchorElement): Promise<void> async function init(signal: AbortSignal): Promise<false | void> { // Do it as soon as possible, before the page loads - const workflows = await getWorkflowsDetails(); + const workflows = await workflowDetails.get(); if (!workflows) { return false; } diff --git a/source/features/highlight-collaborators-and-own-conversations.tsx b/source/features/highlight-collaborators-and-own-conversations.tsx index fba1491c..388c0aac 100644 --- a/source/features/highlight-collaborators-and-own-conversations.tsx +++ b/source/features/highlight-collaborators-and-own-conversations.tsx @@ -1,5 +1,5 @@ import './highlight-collaborators-and-own-conversations.css'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import domLoaded from 'dom-loaded'; import * as pageDetect from 'github-url-detection'; @@ -8,22 +8,23 @@ import features from '../feature-manager.js'; import fetchDom from '../helpers/fetch-dom.js'; import {buildRepoURL, cacheByRepo, getUsername} from '../github-helpers/index.js'; -const getCollaborators = cache.function('repo-collaborators', async (): Promise<string[]> => { - const dom = await fetchDom(buildRepoURL('issues/show_menu_content?partial=issues/filters/authors_content')); - return select - .all('.SelectMenu-item img[alt]', dom) - .map(avatar => avatar.alt.slice(1)); -}, { +const collaborators = new CachedFunction('repo-collaborators', { + async updater(): Promise<string[]> { + const dom = await fetchDom(buildRepoURL('issues/show_menu_content?partial=issues/filters/authors_content')); + return select + .all('.SelectMenu-item img[alt]', dom) + .map(avatar => avatar.alt.slice(1)); + }, maxAge: {days: 1}, staleWhileRevalidate: {days: 20}, cacheKey: cacheByRepo, }); async function highlightCollaborators(): Promise<void> { - const collaborators = await getCollaborators(); + const list = await collaborators.get(); await domLoaded; for (const author of select.all('.js-issue-row [data-hovercard-type="user"]')) { - if (collaborators.includes(author.textContent!.trim())) { + if (list.includes(author.textContent!.trim())) { author.classList.add('rgh-collaborator'); } } diff --git a/source/features/link-to-changelog-file.tsx b/source/features/link-to-changelog-file.tsx index e41a4544..f28125c1 100644 --- a/source/features/link-to-changelog-file.tsx +++ b/source/features/link-to-changelog-file.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import {BookIcon} from '@primer/octicons-react'; import elementReady from 'element-ready'; @@ -8,15 +8,13 @@ import * as pageDetect from 'github-url-detection'; import features from '../feature-manager.js'; import api from '../github-helpers/api.js'; import {wrapAll} from '../helpers/dom-utils.js'; -import {buildRepoURL, cacheByRepo} from '../github-helpers/index.js'; +import {buildRepoURL, getRepo} from '../github-helpers/index.js'; type FileType = { name: string; type: string; }; -const cacheName = 'changelog'; - const changelogFiles = /^(changelog|news|changes|history|release|whatsnew)(\.(mdx?|mkdn?|mdwn|mdown|markdown|litcoffee|txt|rst))?$/i; function findChangelogName(files: string[]): string | false { return files.find(name => changelogFiles.test(name)) ?? false; @@ -24,12 +22,17 @@ function findChangelogName(files: string[]): string | false { function parseFromDom(): false { const files = select.all('[aria-labelledby="files"] .js-navigation-open[href*="/blob/"').map(file => file.title); - void cache.set(cacheName + ':' + cacheByRepo(), findChangelogName(files)); + void changelogName.applyOverride( + [findChangelogName(files) as string] /* TODO: Type mistake */, + getRepo()!.nameWithOwner, + ); return false; } -const getChangelogName = cache.function(cacheName, async (): Promise<string | false> => { - const {repository} = await api.v4(` +const changelogName = new CachedFunction('changelog', { + async updater(nameWithOwner: string): Promise<string | false> { + const [owner, name] = nameWithOwner.split('/'); + const {repository} = await api.v4(` repository() { object(expression: "HEAD:") { ...on Tree { @@ -40,22 +43,23 @@ const getChangelogName = cache.function(cacheName, async (): Promise<string | fa } } } - `); + `, { + variables: {name, owner}, + }); - const files: string[] = []; - for (const entry of repository.object.entries as FileType[]) { - if (entry.type === 'blob') { - files.push(entry.name); + const files: string[] = []; + for (const entry of repository.object.entries as FileType[]) { + if (entry.type === 'blob') { + files.push(entry.name); + } } - } - return findChangelogName(files); -}, { - cacheKey: cacheByRepo, + return findChangelogName(files); + }, }); async function init(): Promise<void | false> { - const changelog = await getChangelogName(); + const changelog = await changelogName.get(getRepo()!.nameWithOwner); if (!changelog) { return false; } @@ -100,3 +104,15 @@ void features.add(import.meta.url, { awaitDomReady: true, // Does not affect current visit init: parseFromDom, }); + +/* + +Test URLs: + +- CHANGELOG.md: https://github.com/nodeca/js-yaml/releases/tag/4.0.0) +- CHANGELOG.rst: https://github.com/pyca/cryptography/releases) +- CHANGES: https://github.com/sphinx-doc/sphinx/releases) +- news: https://github.com/pypa/pip/releases) +- HISTORY.md: https://github.com/psf/requests/releases) + +*/ diff --git a/source/features/list-prs-for-branch.tsx b/source/features/list-prs-for-branch.tsx index ae61b6f4..d4994139 100644 --- a/source/features/list-prs-for-branch.tsx +++ b/source/features/list-prs-for-branch.tsx @@ -3,7 +3,7 @@ import React from 'dom-chef'; import features from '../feature-manager.js'; import getCurrentGitRef from '../github-helpers/get-current-git-ref.js'; import isDefaultBranch from '../github-helpers/is-default-branch.js'; -import {getPullRequestsAssociatedWithBranch, stateIcon} from './show-associated-branch-prs-on-fork.js'; +import {pullRequestsAssociatedWithBranch, stateIcon} from './show-associated-branch-prs-on-fork.js'; import {addAfterBranchSelector, isPermalink, isRepoCommitListRoot} from '../github-helpers/index.js'; import observe from '../helpers/selector-observer.js'; import api from '../github-helpers/api.js'; @@ -18,7 +18,7 @@ const stateColorMap = { }; async function add(branchSelectorParent: HTMLDetailsElement): Promise<void | false> { - const getPr = await getPullRequestsAssociatedWithBranch(); + const getPr = await pullRequestsAssociatedWithBranch.get(); const currentBranch = getCurrentGitRef()!; const prInfo = getPr[currentBranch]; if (!prInfo) { diff --git a/source/features/list-prs-for-file.tsx b/source/features/list-prs-for-file.tsx index 40d51fdf..eb84bf54 100644 --- a/source/features/list-prs-for-file.tsx +++ b/source/features/list-prs-for-file.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import {isFirefox} from 'webext-detect-page'; import * as pageDetect from 'github-url-detection'; import {AlertIcon, GitPullRequestIcon} from '@primer/octicons-react'; @@ -57,26 +57,27 @@ function getDropdown(prs: number[]): HTMLElement { /** @returns prsByFile {"filename1": [10, 3], "filename2": [2]} */ -const getPrsByFile = cache.function('files-with-prs', async (): Promise<Record<string, number[]>> => { - const {repository} = await api.v4(listPrsForFileQuery, { - variables: { - defaultBranch: await getDefaultBranch(), - }, - }); - - const files: Record<string, number[]> = {}; - - for (const pr of repository.pullRequests.nodes) { - for (const {path} of pr.files.nodes) { - files[path] = files[path] ?? []; - if (files[path].length < 10) { - files[path].push(pr.number); +const getPrsByFile = new CachedFunction('files-with-prs', { + async updater(): Promise<Record<string, number[]>> { + const {repository} = await api.v4(listPrsForFileQuery, { + variables: { + defaultBranch: await getDefaultBranch(), + }, + }); + + const files: Record<string, number[]> = {}; + + for (const pr of repository.pullRequests.nodes) { + for (const {path} of pr.files.nodes) { + files[path] = files[path] ?? []; + if (files[path].length < 10) { + files[path].push(pr.number); + } } } - } - return files; -}, { + return files; + }, maxAge: {hours: 2}, staleWhileRevalidate: {days: 9}, cacheKey: cacheByRepo, @@ -84,7 +85,7 @@ const getPrsByFile = cache.function('files-with-prs', async (): Promise<Record<s async function addToSingleFile(moreFileActionsDropdown: HTMLElement): Promise<void> { const path = new GitHubURL(location.href).filePath; - const prsByFile = await getPrsByFile(); + const prsByFile = await getPrsByFile.get(); const prs = prsByFile[path]; if (prs) { @@ -99,7 +100,7 @@ async function addToSingleFile(moreFileActionsDropdown: HTMLElement): Promise<vo async function addToEditingFile(saveButton: HTMLElement): Promise<false | void> { const path = new GitHubURL(location.href).filePath; - const prsByFile = await getPrsByFile(); + const prsByFile = await getPrsByFile.get(); let prs = prsByFile[path]; if (!prs) { diff --git a/source/features/mark-private-orgs.tsx b/source/features/mark-private-orgs.tsx index ad0a2dfa..60c0ccea 100644 --- a/source/features/mark-private-orgs.tsx +++ b/source/features/mark-private-orgs.tsx @@ -1,6 +1,6 @@ import './mark-private-orgs.css'; import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import {EyeClosedIcon} from '@primer/octicons-react'; import * as pageDetect from 'github-url-detection'; @@ -9,12 +9,13 @@ import features from '../feature-manager.js'; import api from '../github-helpers/api.js'; import {getUsername} from '../github-helpers/index.js'; -const getPublicOrganizationsNames = cache.function('public-organizations', async (username: string): Promise<string[]> => { +const publicOrganizationsNames = new CachedFunction('public-organizations', { + async updater(username: string): Promise<string[]> { // API v4 seems to *require* `org:read` permission AND it includes private organizations as well, which defeats the purpose. There's no way to filter them. // GitHub's API explorer inexplicably only includes public organizations. - const response = await api.v3(`/users/${username}/orgs`); - return response.map((organization: AnyObject) => organization.login); -}, { + const response = await api.v3(`/users/${username}/orgs`); + return response.map((organization: AnyObject) => organization.login); + }, maxAge: {hours: 6}, staleWhileRevalidate: {days: 10}, }); @@ -25,9 +26,9 @@ async function init(): Promise<false | void> { return false; } - const publicOrganizationsNames = await getPublicOrganizationsNames(getUsername()!); + const organizations = await publicOrganizationsNames.get(getUsername()!); for (const org of orgs) { - if (!publicOrganizationsNames.includes(org.pathname.replace(/^\/(organizations\/)?/, ''))) { + if (!organizations.includes(org.pathname.replace(/^\/(organizations\/)?/, ''))) { org.classList.add('rgh-private-org'); org.append(<EyeClosedIcon/>); } diff --git a/source/features/pinned-issues-update-time.tsx b/source/features/pinned-issues-update-time.tsx index e4b617af..8b22c085 100644 --- a/source/features/pinned-issues-update-time.tsx +++ b/source/features/pinned-issues-update-time.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import batchedFunction from 'batched-function'; import * as pageDetect from 'github-url-detection'; @@ -14,8 +14,9 @@ type IssueInfo = { updatedAt: string; }; -const getLastUpdated = cache.function('last-updated', async (issueNumbers: number[]): Promise<Record<string, IssueInfo>> => { - const {repository} = await api.v4(` +const getLastUpdated = new CachedFunction('last-updated', { + async updater(issueNumbers: number[]): Promise<Record<string, IssueInfo>> { + const {repository} = await api.v4(` repository() { ${issueNumbers.map(number => ` ${api.escapeKey(number)}: issue(number: ${number}) { @@ -25,8 +26,8 @@ const getLastUpdated = cache.function('last-updated', async (issueNumbers: numbe } `); - return repository; -}, { + return repository; + }, maxAge: {minutes: 30}, cacheKey: ([issues]) => `${getRepo()!.nameWithOwner}:${String(issues)}`, }); @@ -36,7 +37,7 @@ function getPinnedIssueNumber(pinnedIssue: HTMLElement): number { } const update = batchedFunction(async (pinnedIssues: HTMLElement[]): Promise<void | false> => { - const lastUpdated: Record<string, IssueInfo> = await getLastUpdated(pinnedIssues.map(issue => getPinnedIssueNumber(issue))); + const lastUpdated: Record<string, IssueInfo> = await getLastUpdated.get(pinnedIssues.map(issue => getPinnedIssueNumber(issue))); for (const pinnedIssue of pinnedIssues) { const issueNumber = getPinnedIssueNumber(pinnedIssue); const {updatedAt} = lastUpdated[api.escapeKey(issueNumber)]; diff --git a/source/features/pr-commit-lines-changed.tsx b/source/features/pr-commit-lines-changed.tsx index 58bfddb0..7930ce8a 100644 --- a/source/features/pr-commit-lines-changed.tsx +++ b/source/features/pr-commit-lines-changed.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import elementReady from 'element-ready'; import * as pageDetect from 'github-url-detection'; @@ -7,8 +7,9 @@ import features from '../feature-manager.js'; import api from '../github-helpers/api.js'; import pluralize from '../helpers/pluralize.js'; -const getCommitChanges = cache.function('commit-changes', async (commit: string): Promise<[additions: number, deletions: number]> => { - const {repository} = await api.v4(` +const commitChanges = new CachedFunction('commit-changes', { + async updater(commit: string): Promise<[additions: number, deletions: number]> { + const {repository} = await api.v4(` query getCommitChanges($owner: String!, $name: String!, $commit: String!) { repository(owner: $owner, name: $name) { object(expression: $commit) { @@ -20,17 +21,17 @@ const getCommitChanges = cache.function('commit-changes', async (commit: string) } } `, { - variables: { - commit, - }, - }); + variables: { + commit, + }, + }); - return [repository.object.additions, repository.object.deletions]; -}); + return [repository.object.additions, repository.object.deletions]; + }}); async function init(): Promise<void> { const commitSha = location.pathname.split('/').pop()!; - const [additions, deletions] = await getCommitChanges(commitSha); + const [additions, deletions] = await commitChanges.get(commitSha); const tooltip = pluralize(additions + deletions, '1 line changed', '$$ lines changed'); const diffstat = await elementReady('.diffstat', {waitForChildren: false}); diffstat!.replaceWith( diff --git a/source/features/pr-filters.tsx b/source/features/pr-filters.tsx index 9303189b..b740ede0 100644 --- a/source/features/pr-filters.tsx +++ b/source/features/pr-filters.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import {CheckIcon} from '@primer/octicons-react'; import * as pageDetect from 'github-url-detection'; @@ -52,8 +52,9 @@ function addDraftFilter(dropdown: HTMLElement): void { addDropdownItem(dropdown, 'Not ready for review (Draft PR)', 'draft', 'true'); } -const hasChecks = cache.function('has-checks', async (): Promise<boolean> => { - const {repository} = await api.v4(` +const hasChecks = new CachedFunction('has-checks', { + async updater(): Promise<boolean> { + const {repository} = await api.v4(` repository() { head: object(expression: "HEAD") { ... on Commit { @@ -69,14 +70,14 @@ const hasChecks = cache.function('has-checks', async (): Promise<boolean> => { } `); - return repository.head.history.nodes.some((commit: AnyObject) => commit.statusCheckRollup); -}, { + return repository.head.history.nodes.some((commit: AnyObject) => commit.statusCheckRollup); + }, maxAge: {days: 3}, cacheKey: cacheByRepo, }); async function addChecksFilter(reviewsFilter: HTMLElement): Promise<void> { - if (!await hasChecks()) { + if (!await hasChecks.get()) { return; } diff --git a/source/features/profile-gists-link.tsx b/source/features/profile-gists-link.tsx index f5c3ddbc..faaaa0cb 100644 --- a/source/features/profile-gists-link.tsx +++ b/source/features/profile-gists-link.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import * as pageDetect from 'github-url-detection'; import {CodeSquareIcon} from '@primer/octicons-react'; @@ -10,8 +10,9 @@ import {getCleanPathname} from '../github-helpers/index.js'; import createDropdownItem from '../github-helpers/create-dropdown-item.js'; import observe from '../helpers/selector-observer.js'; -const getGistCount = cache.function('gist-count', async (username: string): Promise<number> => { - const {user} = await api.v4(` +const gistCount = new CachedFunction('gist-count', { + async updater(username: string): Promise<number> { + const {user} = await api.v4(` query getGistCount($username: String!) { user(login: $username) { gists(first: 0) { @@ -20,10 +21,10 @@ const getGistCount = cache.function('gist-count', async (username: string): Prom } } `, { - variables: {username}, - }); - return user.gists.totalCount; -}, { + variables: {username}, + }); + return user.gists.totalCount; + }, maxAge: {days: 1}, staleWhileRevalidate: {days: 3}, }); @@ -62,7 +63,7 @@ async function appendTab(navigationBar: Element): Promise<void> { ); } - const count = await getGistCount(user.name); + const count = await gistCount.get(user.name); if (count > 0) { link.append(<span className="Counter">{count}</span>); } diff --git a/source/features/releases-dropdown.tsx b/source/features/releases-dropdown.tsx index bb666de5..f9614d85 100644 --- a/source/features/releases-dropdown.tsx +++ b/source/features/releases-dropdown.tsx @@ -1,7 +1,7 @@ import React from 'dom-chef'; import * as pageDetect from 'github-url-detection'; import delegate, {DelegateEvent} from 'delegate-it'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import api from '../github-helpers/api.js'; import features from '../feature-manager.js'; @@ -18,10 +18,11 @@ const gql = ` } `; -const getReleases = cache.function('releases', async (): Promise<string[]> => { - const {repository} = await api.v4(gql); - return repository.releases.nodes.map(({tagName}: {tagName: string}) => tagName); -}, { +const getReleases = new CachedFunction('releases', { + async updater(): Promise<string[]> { + const {repository} = await api.v4(gql); + return repository.releases.nodes.map(({tagName}: {tagName: string}) => tagName); + }, maxAge: {hours: 1}, staleWhileRevalidate: {days: 4}, cacheKey: cacheByRepo, @@ -31,7 +32,7 @@ const getReleases = cache.function('releases', async (): Promise<string[]> => { async function selectionHandler(event: DelegateEvent<Event, HTMLInputElement>): Promise<void> { const field = event.delegateTarget; const selectedTag = field.value; - const releases = await getReleases(); // Expected to be in cache + const releases = await getReleases.get(); // Expected to be in cache if (!('inputType' in event) && releases.includes(selectedTag)) { location.href = buildRepoURL('releases/tag', encodeURIComponent(selectedTag)); field.value = ''; // Can't call `preventDefault`, the `input` event is not cancelable @@ -39,7 +40,7 @@ async function selectionHandler(event: DelegateEvent<Event, HTMLInputElement>): } async function addList(searchField: HTMLInputElement): Promise<void> { - const releases = await getReleases(); + const releases = await getReleases.get(); if (releases.length === 0) { return; } diff --git a/source/features/releases-tab.tsx b/source/features/releases-tab.tsx index d5b97ff1..db390e53 100644 --- a/source/features/releases-tab.tsx +++ b/source/features/releases-tab.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import {TagIcon} from '@primer/octicons-react'; import elementReady from 'element-ready'; @@ -11,13 +11,11 @@ import api from '../github-helpers/api.js'; import looseParseInt from '../helpers/loose-parse-int.js'; import abbreviateNumber from '../helpers/abbreviate-number.js'; import createDropdownItem from '../github-helpers/create-dropdown-item.js'; -import {buildRepoURL, cacheByRepo} from '../github-helpers/index.js'; +import {buildRepoURL, cacheByRepo, getRepo} from '../github-helpers/index.js'; import {releasesSidebarSelector} from './clean-repo-sidebar.js'; import {appendBefore, highlightTab, unhighlightTab} from '../helpers/dom-utils.js'; import {underlineNavDropdownUl} from '../github-helpers/selectors.js'; -const cacheName = 'releases-count'; - async function parseCountFromDom(): Promise<number> { const moreReleasesCountElement = await elementReady(releasesSidebarSelector + ' .Counter'); if (moreReleasesCountElement) { @@ -27,14 +25,17 @@ async function parseCountFromDom(): Promise<number> { return 0; } -async function fetchFromApi(): Promise<number> { +async function fetchFromApi(nameWithOwner: string): Promise<number> { + const [owner, name] = nameWithOwner.split('/'); const {repository} = await api.v4(` - repository() { + repository(owner: $owner, name: $name) { releases { totalCount } } - `); + `, { + variables: {name, owner}, + }); return repository.releases.totalCount; } @@ -43,19 +44,20 @@ async function fetchFromApi(): Promise<number> { // - It is disabled by repository owner on the home page (release DOM element won't be there) // - It only contains pre-releases (count badge won't be shown) // For this reason, if we can't find a count from the DOM, we ask the API instead (see #6298) -export const getReleaseCount = cache.function(cacheName, async () => await parseCountFromDom() || fetchFromApi(), { +export const releasesCount = new CachedFunction('releases-count', { + updater: async (nameWithOwner: string) => await parseCountFromDom() || fetchFromApi(nameWithOwner), maxAge: {hours: 1}, staleWhileRevalidate: {days: 3}, cacheKey: cacheByRepo, }); async function addReleasesTab(): Promise<false | void> { - // Always prefer the information in the DOM - if (pageDetect.isRepoRoot()) { - await cache.delete(cacheName + ':' + cacheByRepo()); - } + const repo = getRepo()!.nameWithOwner; + const count = pageDetect.isRepoRoot() + // Always prefer the information in the DOM + ? await releasesCount.getFresh(repo) + : await releasesCount.get(repo); - const count = await getReleaseCount(); if (count === 0) { return false; } diff --git a/source/features/repo-age.tsx b/source/features/repo-age.tsx index e132f7a5..b250d515 100644 --- a/source/features/repo-age.tsx +++ b/source/features/repo-age.tsx @@ -1,5 +1,5 @@ import twas from 'twas'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import React from 'dom-chef'; import {RepoIcon} from '@primer/octicons-react'; import elementReady from 'element-ready'; @@ -72,8 +72,9 @@ async function getRepoAge(commitSha: string, commitsCount: number): Promise<[com return [committedDate, resourcePath]; } -const getFirstCommit = cache.function('first-commit', async (): Promise<[committedDate: string, resourcePath: string]> => { - const {repository} = await api.v4(` +const firstCommit = new CachedFunction('first-commit', { + async updater(): Promise<[committedDate: string, resourcePath: string]> { + const {repository} = await api.v4(` repository() { defaultBranchRef { target { @@ -90,19 +91,19 @@ const getFirstCommit = cache.function('first-commit', async (): Promise<[committ } `); - const {oid: commitSha, history, committedDate, resourcePath} = repository.defaultBranchRef.target as CommitTarget; - const commitsCount = history.totalCount; - if (commitsCount === 1) { - return [committedDate, resourcePath]; - } + const {oid: commitSha, history, committedDate, resourcePath} = repository.defaultBranchRef.target as CommitTarget; + const commitsCount = history.totalCount; + if (commitsCount === 1) { + return [committedDate, resourcePath]; + } - return getRepoAge(commitSha, commitsCount); -}, { + return getRepoAge(commitSha, commitsCount); + }, cacheKey: cacheByRepo, }); async function init(): Promise<void> { - const [firstCommitDate, firstCommitHref] = await getFirstCommit()!; + const [firstCommitDate, firstCommitHref] = await firstCommit.get()!; const birthday = new Date(firstCommitDate); // `twas` could also return `an hour ago` or `just now` diff --git a/source/features/rgh-feature-descriptions.tsx b/source/features/rgh-feature-descriptions.tsx index 455ecdcf..6fed4cc9 100644 --- a/source/features/rgh-feature-descriptions.tsx +++ b/source/features/rgh-feature-descriptions.tsx @@ -2,14 +2,13 @@ import './rgh-feature-descriptions.css'; import React from 'dom-chef'; import * as pageDetect from 'github-url-detection'; import {AlertIcon, CopyIcon, InfoIcon} from '@primer/octicons-react'; -import cache from 'webext-storage-cache'; import features from '../feature-manager.js'; import {featuresMeta} from '../../readme.md'; import optionsStorage, {getNewFeatureName, isFeatureDisabled} from '../options-storage.js'; import {isRefinedGitHubRepo} from '../github-helpers/index.js'; import observe from '../helpers/selector-observer.js'; -import {HotfixStorage} from '../helpers/hotfix.js'; +import {brokenFeatures} from '../helpers/hotfix.js'; import {createRghIssueLink} from '../helpers/rgh-issue-link.js'; import openOptions from '../helpers/open-options.js'; import createBanner from '../github-helpers/banner.js'; @@ -86,7 +85,7 @@ function addDescription(infoBanner: HTMLElement, id: string, meta: FeatureMeta | async function getDisabledReason(id: string): Promise<JSX.Element | undefined> { const classes = ['mb-3']; // Skip dev check present in `getLocalHotfixes`, we want to see this even when developing - const hotfixes = await cache.get<HotfixStorage>('hotfixes:') ?? []; + const hotfixes = await brokenFeatures.get() ?? []; const hotfixed = hotfixes.find(([feature]) => feature === id); if (hotfixed) { const [_name, issue, unaffectedVersion] = hotfixed; diff --git a/source/features/rgh-sponsor-button.tsx b/source/features/rgh-sponsor-button.tsx deleted file mode 100644 index c6d03f5f..00000000 --- a/source/features/rgh-sponsor-button.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/* - - .. : - . . . . . - . . . .. . . * - * . .. . - . . . : . . . . - . . . . . . - . . *:. . . -. . . . .. . . - . . . . ... . . - . . . . . . . . . - . . . ... .. . . . - . . . *. . . - . :. . . - . . . . - . . . ./|\ - . .. :. . | . . - . ... . | - . :. . . *. | . . - . *. You are here. - . . . . *. . - -*/ -import cache from 'webext-storage-cache'; -import select from 'select-dom'; -import * as pageDetect from 'github-url-detection'; -import delegate, {DelegateEvent} from 'delegate-it'; - -import features from '../feature-manager.js'; -import {getRepo, getUsername} from '../github-helpers/index.js'; - -async function wiggleWiggleWiggle(): Promise<void> { - await cache.set('did-it-wiggle', 'yup', {days: 7}); - select('#sponsor-button-repo')?.animate({ - transform: [ - 'none', - 'rotate(-2deg) scale(1.05)', - 'rotate(2deg) scale(1.1)', - 'rotate(-2deg) scale(1.1)', - 'rotate(2deg) scale(1.1)', - 'rotate(-2deg) scale(1.1)', - 'rotate(2deg) scale(1.05)', - 'none', - ], - }, 600); -} - -async function suchLove({delegateTarget}: DelegateEvent): Promise<void> { - const heart = select('.octicon-heart', delegateTarget); - - // .closest ensures that clicking the lightbox’ background doesn't also trigger the animation - if (!heart || delegateTarget.closest('details[open]')) { - return; - } - - const rect = heart.getBoundingClientRect(); - const love = heart.cloneNode(true); - Object.assign(love.style, { - position: 'fixed', - zIndex: '9999999999', - left: `${rect.x}px`, - top: `${rect.y}px`, - }); - - document.body.append(love); - - await love.animate({ - transform: [ - 'translateZ(0)', - 'translateZ(0) scale(80)', - ], - opacity: [ - 1, - 0, - ], - }, { - duration: 600, - easing: 'ease-out', - }).finished; - - love.remove(); // 💔 -} - -async function handleNewIssue(signal: AbortSignal): Promise<false> { - if (getRepo()!.owner !== getUsername() && !await cache.get('did-it-wiggle')) { - select([ - '.btn-primary[href$="/issues/new/choose"]', - '.btn-primary[href$="/issues/new"]', - ]) - ?.addEventListener('mouseenter', wiggleWiggleWiggle, {once: true, signal}); - } - - return false; -} - -function handleSponsorButton(signal: AbortSignal): void { - delegate('#sponsor-button-repo, #sponsor-profile-button, [aria-label^="Sponsor @"]', 'click', suchLove, {signal}); -} - -void features.add(import.meta.url, { - include: [ - pageDetect.isIssue, - pageDetect.isRepoIssueList, - ], - awaitDomReady: true, // 🤷♂️ - init: handleNewIssue, -}, { - include: [ - pageDetect.hasRepoHeader, - pageDetect.isUserProfile, - pageDetect.isOrganizationProfile, - ], - exclude: [ - pageDetect.isOwnUserProfile, - pageDetect.isPrivateUserProfile, - ], - init: handleSponsorButton, -}); diff --git a/source/features/show-associated-branch-prs-on-fork.tsx b/source/features/show-associated-branch-prs-on-fork.tsx index 2712915c..3720ede1 100644 --- a/source/features/show-associated-branch-prs-on-fork.tsx +++ b/source/features/show-associated-branch-prs-on-fork.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import * as pageDetect from 'github-url-detection'; import {GitMergeIcon, GitPullRequestIcon, GitPullRequestClosedIcon, GitPullRequestDraftIcon} from '@primer/octicons-react'; @@ -18,8 +18,9 @@ type PullRequest = { url: string; }; -export const getPullRequestsAssociatedWithBranch = cache.function('associatedBranchPullRequests', async (): Promise<Record<string, PullRequest>> => { - const {repository} = await api.v4(` +export const pullRequestsAssociatedWithBranch = new CachedFunction('associatedBranchPullRequests', { + async updater(): Promise<Record<string, PullRequest>> { + const {repository} = await api.v4(` repository() { refs(refPrefix: "refs/heads/", last: 100) { nodes { @@ -42,19 +43,19 @@ export const getPullRequestsAssociatedWithBranch = cache.function('associatedBra } `); - const pullRequests: Record<string, PullRequest> = {}; - for (const {name, associatedPullRequests} of repository.refs.nodes) { - const [prInfo] = associatedPullRequests.nodes as PullRequest[]; - // Check if the ref was deleted, since the result includes pr's that are not in fact related to this branch but rather to the branch name. - const headRefWasDeleted = prInfo?.timelineItems.nodes[0]?.__typename === 'HeadRefDeletedEvent'; - if (prInfo && !headRefWasDeleted) { - prInfo.state = prInfo.isDraft && prInfo.state === 'OPEN' ? 'DRAFT' : prInfo.state; - pullRequests[name] = prInfo; + const pullRequests: Record<string, PullRequest> = {}; + for (const {name, associatedPullRequests} of repository.refs.nodes) { + const [prInfo] = associatedPullRequests.nodes as PullRequest[]; + // Check if the ref was deleted, since the result includes pr's that are not in fact related to this branch but rather to the branch name. + const headRefWasDeleted = prInfo?.timelineItems.nodes[0]?.__typename === 'HeadRefDeletedEvent'; + if (prInfo && !headRefWasDeleted) { + prInfo.state = prInfo.isDraft && prInfo.state === 'OPEN' ? 'DRAFT' : prInfo.state; + pullRequests[name] = prInfo; + } } - } - return pullRequests; -}, { + return pullRequests; + }, maxAge: {hours: 1}, staleWhileRevalidate: {days: 4}, cacheKey: cacheByRepo, @@ -92,9 +93,9 @@ function addAssociatedPRLabel(branchCompareLink: Element, prInfo: PullRequest): } async function addLink(branchCompareLink: Element): Promise<void> { - const associatedPullRequests = await getPullRequestsAssociatedWithBranch(); + const prs = await pullRequestsAssociatedWithBranch.get(); const branchName = branchCompareLink.closest('[branch]')!.getAttribute('branch')!; - const prInfo = associatedPullRequests[branchName]; + const prInfo = prs[branchName]; if (prInfo) { addAssociatedPRLabel(branchCompareLink, prInfo); } @@ -113,3 +114,11 @@ void features.add(import.meta.url, { ], init, }); + +/* + +Test URLs: + +https://github.com/pnarielwala/create-react-app-ts/branches + +*/ diff --git a/source/features/show-open-prs-of-forks.tsx b/source/features/show-open-prs-of-forks.tsx index ea341d04..94a83850 100644 --- a/source/features/show-open-prs-of-forks.tsx +++ b/source/features/show-open-prs-of-forks.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import select from 'select-dom'; import elementReady from 'element-ready'; import * as pageDetect from 'github-url-detection'; @@ -13,8 +13,9 @@ function getLinkCopy(count: number): string { return pluralize(count, 'one open pull request', 'at least $$ open pull requests'); } -const countPRs = cache.function('prs-on-forked-repo', async (forkedRepo: string): Promise<[prCount: number, singlePrNumber?: number]> => { - const {search} = await api.v4(` +const countPRs = new CachedFunction('prs-on-forked-repo', { + async updater(forkedRepo: string): Promise<{count: number; firstPr?: number}> { + const {search} = await api.v4(` query getPRs($query: String!) { search( first: 100, @@ -32,21 +33,21 @@ const countPRs = cache.function('prs-on-forked-repo', async (forkedRepo: string) } } `, { - variables: { - query: `is:pr is:open archived:false repo:${forkedRepo} author:${getUsername()!}`, - }, - }); + variables: { + query: `is:pr is:open archived:false repo:${forkedRepo} author:${getUsername()!}`, + }, + }); - // Only show PRs originated from the current repo - const prs = search.nodes.filter((pr: AnyObject) => pr.headRepository.nameWithOwner === getRepo()!.nameWithOwner); + // Only show PRs originated from the current repo + const prs = search.nodes.filter((pr: AnyObject) => pr.headRepository.nameWithOwner === getRepo()!.nameWithOwner); - // If only one is found, pass the PR number so we can link to the PR directly - if (prs.length === 1) { - return [1, prs[0].number]; - } + // If only one is found, pass the PR number so we can link to the PR directly + if (prs.length === 1) { + return {count: 1, firstPr: prs[0].number}; + } - return [prs.length]; -}, { + return {count: prs.length}; + }, maxAge: {hours: 1}, staleWhileRevalidate: {days: 2}, cacheKey: ([forkedRepo]): string => `${forkedRepo}:${getRepo()!.nameWithOwner}`, @@ -61,7 +62,7 @@ async function getPRs(): Promise<[prCount: number, url: string] | []> { } const forkedRepo = getForkedRepo()!; - const [count, firstPr] = await countPRs(forkedRepo); + const {count, firstPr} = await countPRs.get(forkedRepo); if (count === 1) { return [count, `/${forkedRepo}/pull/${firstPr!}`]; } diff --git a/source/features/tags-on-commits-list.tsx b/source/features/tags-on-commits-list.tsx index e9ac6809..199979f7 100644 --- a/source/features/tags-on-commits-list.tsx +++ b/source/features/tags-on-commits-list.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import cache from 'webext-storage-cache/legacy.js'; import select from 'select-dom'; import {TagIcon} from '@primer/octicons-react'; import * as pageDetect from 'github-url-detection'; @@ -176,3 +176,11 @@ void features.add(import.meta.url, { deduplicate: 'has-rgh-inner', init, }); + +/* + +Test URLs: + +https://github.com/refined-github/refined-github/commits/19.5.21.1921 + +*/ diff --git a/source/features/toggle-files-button.tsx b/source/features/toggle-files-button.tsx index f592690a..947bac75 100644 --- a/source/features/toggle-files-button.tsx +++ b/source/features/toggle-files-button.tsx @@ -1,6 +1,6 @@ import './toggle-files-button.css'; import select from 'select-dom'; -import cache from 'webext-storage-cache'; +import {CachedValue} from 'webext-storage-cache'; import React from 'dom-chef'; import delegate, {DelegateEvent} from 'delegate-it'; import * as pageDetect from 'github-url-detection'; @@ -10,7 +10,7 @@ import features from '../feature-manager.js'; import observe from '../helpers/selector-observer.js'; import {isHasSelectorSupported} from '../helpers/select-has.js'; -const cacheKey = 'files-hidden'; +const wereFilesHidden = new CachedValue<boolean>('files-hidden'); const toggleButtonClass = 'rgh-toggle-files'; function addButton(filesBox: HTMLElement): void { @@ -50,7 +50,7 @@ async function toggleList(): Promise<void> { firstCollapseOnDesktop(targets); if (window.matchMedia('(min-width: 768px)').matches) { // We just hid the file list, no further action is necessary on desktop - await cache.set<boolean>(cacheKey, true); + void wereFilesHidden.set(true); return; } @@ -64,14 +64,14 @@ async function toggleList(): Promise<void> { async function updateView(anchor: HTMLHeadingElement): Promise<void> { const filesBox = anchor.parentElement!; addButton(filesBox); - if (await cache.get<boolean>(cacheKey)) { + if (await wereFilesHidden.get()) { // This only applies on desktop; Mobile already always starts collapsed and we're not changing that firstCollapseOnDesktop(); } } async function recordToggle({detail}: DelegateEvent<CustomEvent>): Promise<void> { - await cache.set<boolean>(cacheKey, !detail.open); + await wereFilesHidden.set(!detail.open); } async function init(signal: AbortSignal): Promise<void> { diff --git a/source/features/unreleased-commits.tsx b/source/features/unreleased-commits.tsx index a4addd07..a28f35a5 100644 --- a/source/features/unreleased-commits.tsx +++ b/source/features/unreleased-commits.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import * as pageDetect from 'github-url-detection'; import {TagIcon} from '@primer/octicons-react'; @@ -29,8 +29,9 @@ type Tags = { export const undeterminableAheadBy = Number.MAX_SAFE_INTEGER; // For when the branch is ahead by more than 20 commits #5505 -export const getRepoPublishState = cache.function('tag-ahead-by', async (): Promise<RepoPublishState> => { - const {repository} = await api.v4(` +export const repoPublishState = new CachedFunction('tag-ahead-by', { + async updater(): Promise<RepoPublishState> { + const {repository} = await api.v4(` repository() { refs(first: 20, refPrefix: "refs/tags/", orderBy: { field: TAG_COMMIT_DATE, @@ -62,36 +63,36 @@ export const getRepoPublishState = cache.function('tag-ahead-by', async (): Prom } `); - if (repository.refs.nodes.length === 0) { - return { - latestTag: false, - aheadBy: 0, - }; - } + if (repository.refs.nodes.length === 0) { + return { + latestTag: false, + aheadBy: 0, + }; + } - const tags = new Map<string, string>(); - for (const node of repository.refs.nodes as Tags[]) { - tags.set(node.name, node.tag.commit?.oid ?? node.tag.oid); - } + const tags = new Map<string, string>(); + for (const node of repository.refs.nodes as Tags[]) { + tags.set(node.name, node.tag.commit?.oid ?? node.tag.oid); + } - // If this logic ever gets dropped or becomes simpler, consider using the native "compare" API - // https://github.com/refined-github/refined-github/issues/6094 - const latestTag = getLatestVersionTag([...tags.keys()]); - const latestTagOid = tags.get(latestTag)!; - const aheadBy = repository.defaultBranchRef.target.history.nodes.findIndex((node: AnyObject) => node.oid === latestTagOid); + // If this logic ever gets dropped or becomes simpler, consider using the native "compare" API + // https://github.com/refined-github/refined-github/issues/6094 + const latestTag = getLatestVersionTag([...tags.keys()]); + const latestTagOid = tags.get(latestTag)!; + const aheadBy = repository.defaultBranchRef.target.history.nodes.findIndex((node: AnyObject) => node.oid === latestTagOid); - return { - latestTag, - aheadBy: aheadBy === -1 ? undeterminableAheadBy : aheadBy, - }; -}, { + return { + latestTag, + aheadBy: aheadBy === -1 ? undeterminableAheadBy : aheadBy, + }; + }, maxAge: {hours: 1}, staleWhileRevalidate: {days: 2}, cacheKey: cacheByRepo, }); async function add(branchSelectorParent: HTMLDetailsElement): Promise<void> { - const {latestTag, aheadBy} = await getRepoPublishState(); + const {latestTag, aheadBy} = await repoPublishState.get(); const isAhead = aheadBy > 0; if (!latestTag || !isAhead) { diff --git a/source/features/user-local-time.tsx b/source/features/user-local-time.tsx index 8e52a705..070e1cc6 100644 --- a/source/features/user-local-time.tsx +++ b/source/features/user-local-time.tsx @@ -2,7 +2,7 @@ import './user-local-time.css'; import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import delay from 'delay'; import select from 'select-dom'; import {ClockIcon} from '@primer/octicons-react'; @@ -28,42 +28,43 @@ async function loadCommitPatch(commitUrl: string): Promise<string> { return textContent; } -const getLastCommitDate = cache.function('last-commit', async (login: string): Promise<string | false> => { - for await (const page of api.v3paginated(`/users/${login}/events`)) { - for (const event of page as any) { - if (event.type !== 'PushEvent') { - continue; - } - - // Start from the latest commit, which is the last one in the list - for (const commit of event.payload.commits.reverse() as Commit[]) { - const response = await api.v3(commit.url, {ignoreHTTPStatus: true}); - // Commits might not exist anymore even if they are listed in the events - // This can happen if the repository was deleted so we can also skip all other commits - if (response.httpStatus === 404) { - break; - } - - if (!response.ok) { - throw await api.getError(response); - } - - // `response.author` only appears if GitHub can match the email to a GitHub user - if (response.author?.id !== event.actor.id) { +const lastCommitDate = new CachedFunction('last-commit', { + async updater(login: string): Promise<string | false> { + for await (const page of api.v3paginated(`/users/${login}/events`)) { + for (const event of page as any) { + if (event.type !== 'PushEvent') { continue; } - const patch = await loadCommitPatch(commit.url); - // The patch of merge commits doesn't include the commit sha so the date might be from another user - if (patch.startsWith(`From ${commit.sha} `)) { - return /^Date: (.*)$/m.exec(patch)?.[1] ?? false; + // Start from the latest commit, which is the last one in the list + for (const commit of event.payload.commits.reverse() as Commit[]) { + const response = await api.v3(commit.url, {ignoreHTTPStatus: true}); + // Commits might not exist anymore even if they are listed in the events + // This can happen if the repository was deleted so we can also skip all other commits + if (response.httpStatus === 404) { + break; + } + + if (!response.ok) { + throw await api.getError(response); + } + + // `response.author` only appears if GitHub can match the email to a GitHub user + if (response.author?.id !== event.actor.id) { + continue; + } + + const patch = await loadCommitPatch(commit.url); + // The patch of merge commits doesn't include the commit sha so the date might be from another user + if (patch.startsWith(`From ${commit.sha} `)) { + return /^Date: (.*)$/m.exec(patch)?.[1] ?? false; + } } } } - } - return false; -}, { + return false; + }, maxAge: {days: 10}, staleWhileRevalidate: {days: 20}, }); @@ -122,7 +123,7 @@ async function insertUserLocalTime(hovercardContainer: Element): Promise<void> { return; } - const datePromise = getLastCommitDate(login); + const datePromise = lastCommitDate.get(login); const race = await Promise.race([delay(300), datePromise]); if (race === false) { // The timezone was undeterminable and this resolved "immediately" (or was cached), so don't add the icon at all @@ -167,3 +168,13 @@ function init(signal: AbortSignal): void { void features.add(import.meta.url, { init, }); + +/* + +Test URLs: + +1. Open https://github.com/sindresorhus/np/releases/tag/v8.0.4 +2. Hover over the username "sindresorhus" in the sidebar +3. Notice his local time in the hovercard + +*/ diff --git a/source/features/user-profile-follower-badge.tsx b/source/features/user-profile-follower-badge.tsx index ee3ae41e..8c6ea519 100644 --- a/source/features/user-profile-follower-badge.tsx +++ b/source/features/user-profile-follower-badge.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import elementReady from 'element-ready'; import * as pageDetect from 'github-url-detection'; @@ -8,17 +8,18 @@ import api from '../github-helpers/api.js'; import {getUsername, getCleanPathname} from '../github-helpers/index.js'; import attachElement from '../helpers/attach-element.js'; -const doesUserFollow = cache.function('user-follows', async (userA: string, userB: string): Promise<boolean> => { - const {httpStatus} = await api.v3(`/users/${userA}/following/${userB}`, { - json: false, - ignoreHTTPStatus: true, - }); +const doesUserFollow = new CachedFunction('user-follows', { + async updater(userA: string, userB: string): Promise<boolean> { + const {httpStatus} = await api.v3(`/users/${userA}/following/${userB}`, { + json: false, + ignoreHTTPStatus: true, + }); - return httpStatus === 204; -}); + return httpStatus === 204; + }}); async function init(): Promise<void> { - if (!await doesUserFollow(getCleanPathname(), getUsername()!)) { + if (!await doesUserFollow.get(getCleanPathname(), getUsername()!)) { return; } @@ -40,3 +41,14 @@ void features.add(import.meta.url, { ], init, }); + +/* + +Test URLs: + +1. Visit your own profile +2. Click on "X followers" below your profile picture +3. Click on a follower +4. Look for a "Follows you" badge below their profile picture + +*/ diff --git a/source/features/warn-pr-from-master.tsx b/source/features/warn-pr-from-master.tsx index 11e9e744..4c600ad7 100644 --- a/source/features/warn-pr-from-master.tsx +++ b/source/features/warn-pr-from-master.tsx @@ -3,14 +3,14 @@ import select from 'select-dom'; import * as pageDetect from 'github-url-detection'; import features from '../feature-manager.js'; -import getDefaultBranch, {getDefaultBranchOfRepo} from '../github-helpers/get-default-branch.js'; +import getDefaultBranch, {defaultBranchOfRepo} from '../github-helpers/get-default-branch.js'; import {getRepo} from '../github-helpers/index.js'; async function init(): Promise<false | void> { let defaultBranch; if (select.exists('.is-cross-repo')) { const forkedRepository = getRepo(select('[title^="head: "]')!.textContent!)!; - defaultBranch = await getDefaultBranchOfRepo(forkedRepository); + defaultBranch = await defaultBranchOfRepo.get(forkedRepository); } else { defaultBranch = await getDefaultBranch(); } diff --git a/source/github-helpers/get-default-branch.ts b/source/github-helpers/get-default-branch.ts index 1467ee16..f5e0c5db 100644 --- a/source/github-helpers/get-default-branch.ts +++ b/source/github-helpers/get-default-branch.ts @@ -1,4 +1,4 @@ -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import elementReady from 'element-ready'; import {type RepositoryInfo} from 'github-url-detection'; @@ -41,8 +41,8 @@ async function fromAPI(repository: RepositoryInfo): Promise<string> { // DO NOT use optional arguments/defaults in "cached functions" because they can't be memoized effectively // https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1864 -export const getDefaultBranchOfRepo = cache.function('default-branch', - async (repository: RepositoryInfo): Promise<string> => { +export const defaultBranchOfRepo = new CachedFunction('default-branch', { + async updater(repository: RepositoryInfo): Promise<string> { if (!repository) { throw new Error('getDefaultBranch was called on a non-repository page'); } @@ -50,13 +50,13 @@ export const getDefaultBranchOfRepo = cache.function('default-branch', // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Wrong, the type is `false | undefined` return (isCurrentRepo(repository) && await fromDOM()) || fromAPI(repository); }, - { - maxAge: {hours: 1}, - staleWhileRevalidate: {days: 20}, - cacheKey: ([repository]) => repository.nameWithOwner, - }, + + maxAge: {hours: 1}, + staleWhileRevalidate: {days: 20}, + cacheKey: ([repository]) => repository.nameWithOwner, +}, ); export default async function getDefaultBranch(): Promise<string> { - return getDefaultBranchOfRepo(getRepo()!); + return defaultBranchOfRepo.get(getRepo()!); } diff --git a/source/helpers/bisect.tsx b/source/helpers/bisect.tsx index d7014ca5..c4fc39f0 100644 --- a/source/helpers/bisect.tsx +++ b/source/helpers/bisect.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedValue} from 'webext-storage-cache'; import select from 'select-dom'; import elementReady from 'element-ready'; @@ -7,6 +7,8 @@ import pluralize from './pluralize.js'; import featureLink from './feature-link.js'; import {importedFeatures} from '../../readme.md'; +export const state = new CachedValue<FeatureID[]>('bisect', {maxAge: {minutes: 15}}); + // Split current list of features in half and create an options-like object to be applied on load // Bisecting 4 features: enable 2 // Bisecting 3 features: enable 1 @@ -16,10 +18,10 @@ const getMiddleStep = (list: any[]): number => Math.floor(list.length / 2); async function onChoiceButtonClick({currentTarget: button}: React.MouseEvent<HTMLButtonElement>): Promise<void> { const answer = button.value; - const bisectedFeatures = (await cache.get<FeatureID[]>('bisect'))!; + const bisectedFeatures = (await state.get())!; if (bisectedFeatures.length > 1) { - await cache.set('bisect', answer === 'yes' + await state.set(answer === 'yes' ? bisectedFeatures.slice(0, getMiddleStep(bisectedFeatures)) : bisectedFeatures.slice(getMiddleStep(bisectedFeatures)), ); @@ -42,12 +44,12 @@ async function onChoiceButtonClick({currentTarget: button}: React.MouseEvent<HTM createMessageBox(<>The change or issue is caused by {feature}.</>); } - await cache.delete('bisect'); + await state.delete(); window.removeEventListener('visibilitychange', hideMessage); } async function onEndButtonClick(): Promise<void> { - await cache.delete('bisect'); + await state.delete(); location.reload(); } @@ -65,14 +67,14 @@ function createMessageBox(message: Element | string, extraButtons?: Element): vo } async function hideMessage(): Promise<void> { - if (!await cache.get<FeatureID[]>('bisect')) { + if (!await state.get()) { createMessageBox('Process completed in another tab'); } } export default async function bisectFeatures(): Promise<Record<string, boolean> | void> { // `bisect` stores the list of features to be split in half - const bisectedFeatures = await cache.get<FeatureID[]>('bisect'); + const bisectedFeatures = await state.get(); if (!bisectedFeatures) { return; } diff --git a/source/helpers/clear-cache-handler.ts b/source/helpers/clear-cache-handler.ts index 3571afad..7e427d0a 100644 --- a/source/helpers/clear-cache-handler.ts +++ b/source/helpers/clear-cache-handler.ts @@ -1,7 +1,7 @@ -import cache from 'webext-storage-cache'; +import {globalCache} from 'webext-storage-cache'; export default async function clearCacheHandler(event: MouseEvent): Promise<void> { - await cache.clear(); + await globalCache.clear(); const button = event.target as HTMLButtonElement; const initialText = button.textContent; button.textContent = 'Cache cleared!'; diff --git a/source/helpers/hotfix.tsx b/source/helpers/hotfix.tsx index 7ac7657d..6c6765fb 100644 --- a/source/helpers/hotfix.tsx +++ b/source/helpers/hotfix.tsx @@ -1,5 +1,5 @@ import React from 'dom-chef'; -import cache from 'webext-storage-cache'; +import {CachedFunction} from 'webext-storage-cache'; import {isEnterprise} from 'github-url-detection'; import compareVersions from 'tiny-version-compare'; import {any as concatenateTemplateLiteralTag} from 'code-tag'; @@ -7,6 +7,8 @@ import {any as concatenateTemplateLiteralTag} from 'code-tag'; import {RGHOptions} from '../options-storage.js'; import isDevelopmentVersion from './is-development-version.js'; +const {version: currentVersion} = browser.runtime.getManifest(); + function parseCsv(content: string): string[][] { const lines = []; const [_header, ...rawLines] = content.trim().split('\n'); @@ -33,35 +35,35 @@ async function fetchHotfix(path: string): Promise<string> { return ''; } -export type HotfixStorage = Array<[FeatureID, string, string]>; +type HotfixStorage = Array<[FeatureID, string, string]>; -export const updateHotfixes = cache.function('hotfixes', async (version: string): Promise<HotfixStorage> => { - const content = await fetchHotfix('broken-features.csv'); - if (!content) { - return []; - } +export const brokenFeatures = new CachedFunction('broken-features', { + async updater(): Promise<HotfixStorage> { + const content = await fetchHotfix('broken-features.csv'); + if (!content) { + return []; + } - const storage: HotfixStorage = []; - for (const [featureID, relatedIssue, unaffectedVersion] of parseCsv(content)) { - if (featureID && relatedIssue && (!unaffectedVersion || compareVersions(unaffectedVersion, version) > 0)) { - storage.push([featureID as FeatureID, relatedIssue, unaffectedVersion]); + const storage: HotfixStorage = []; + for (const [featureID, relatedIssue, unaffectedVersion] of parseCsv(content)) { + if (featureID && relatedIssue && (!unaffectedVersion || compareVersions(unaffectedVersion, currentVersion) > 0)) { + storage.push([featureID as FeatureID, relatedIssue, unaffectedVersion]); + } } - } - return storage; -}, { + return storage; + }, maxAge: {hours: 6}, staleWhileRevalidate: {days: 30}, - cacheKey: () => '', }); -export const getStyleHotfix = cache.function('style-hotfixes', - async (version: string): Promise<string> => fetchHotfix(`style/${version}.css`), - { - maxAge: {hours: 6}, - staleWhileRevalidate: {days: 300}, - cacheKey: () => '', - }, +export const styleHotfixes = new CachedFunction('style-hotfixes', { + updater: async (version: string): Promise<string> => fetchHotfix(`style/${version}.css`), + + maxAge: {hours: 6}, + staleWhileRevalidate: {days: 300}, + cacheKey: () => '', +}, ); export async function getLocalHotfixes(): Promise<HotfixStorage> { @@ -71,7 +73,7 @@ export async function getLocalHotfixes(): Promise<HotfixStorage> { return []; } - return await cache.get<HotfixStorage>('hotfixes:') ?? []; + return await brokenFeatures.get() ?? []; } export async function getLocalHotfixesAsOptions(): Promise<Partial<RGHOptions>> { @@ -92,7 +94,6 @@ export async function applyStyleHotfixes(style: string): Promise<void> { document.body.prepend(<style>{style}</style>); } -const stringHotfixesKey = 'strings-hotfixes'; let localStrings: Record<string, string> = {}; export function _(...arguments_: Parameters<typeof concatenateTemplateLiteralTag>): string { const original = concatenateTemplateLiteralTag(...arguments_); @@ -100,18 +101,19 @@ export function _(...arguments_: Parameters<typeof concatenateTemplateLiteralTag } // Updates the local object from the storage to enable synchronous access -export async function getLocalStrings(): Promise<void> { +export async function preloadSyncLocalStrings(): Promise<void> { if (isDevelopmentVersion() || isEnterprise()) { return; } - localStrings = await cache.get<Record<string, string>>(stringHotfixesKey + ':') ?? {}; + localStrings = await localStringsHotfix.get() ?? {}; } -export const updateLocalStrings = cache.function(stringHotfixesKey, async (): Promise<Record<string, string>> => { - const json = await fetchHotfix('strings.json'); - return json ? JSON.parse(json) : {}; -}, { +export const localStringsHotfix = new CachedFunction('strings-hotfixes', { + async updater(): Promise<Record<string, string>> { + const json = await fetchHotfix('strings.json'); + return json ? JSON.parse(json) : {}; + }, maxAge: {hours: 6}, staleWhileRevalidate: {days: 30}, }); diff --git a/source/options.tsx b/source/options.tsx index 6fe62807..277f6950 100644 --- a/source/options.tsx +++ b/source/options.tsx @@ -1,7 +1,6 @@ import 'webext-base-css/webext-base.css'; import './options.css'; import React from 'dom-chef'; -import cache from 'webext-storage-cache'; import domify from 'doma'; import select from 'select-dom'; import fitTextarea from 'fit-textarea'; @@ -14,13 +13,14 @@ import {isEnterprise} from 'github-url-detection'; import featureLink from './helpers/feature-link.js'; import clearCacheHandler from './helpers/clear-cache-handler.js'; -import {getLocalHotfixes} from './helpers/hotfix.js'; +import {getLocalHotfixes, styleHotfixes} from './helpers/hotfix.js'; import {createRghIssueLink} from './helpers/rgh-issue-link.js'; import {importedFeatures, featuresMeta} from '../readme.md'; import getStorageBytesInUse from './helpers/used-storage.js'; import {perDomainOptions} from './options-storage.js'; import isDevelopmentVersion from './helpers/is-development-version.js'; import {doesBrowserActionOpenOptions} from './helpers/feature-utils.js'; +import {state as bisectState} from './helpers/bisect.js'; type Status = { error?: true; @@ -28,6 +28,8 @@ type Status = { scopes?: string[]; }; +const {version} = browser.runtime.getManifest(); + function reportStatus({error, text, scopes}: Status): void { const tokenStatus = select('#validation')!; tokenStatus.textContent = text ?? ''; @@ -162,7 +164,7 @@ async function findFeatureHandler(event: Event): Promise<void> { // TODO: Add support for GHE const options = await perDomainOptions.getOptionsForOrigin().getAll(); const enabledFeatures = importedFeatures.filter(featureId => options['feature:' + featureId]); - await cache.set<FeatureID[]>('bisect', enabledFeatures, {minutes: 5}); + await bisectState.set(enabledFeatures); const button = event.target as HTMLButtonElement; button.disabled = true; @@ -243,7 +245,7 @@ function updateRateLink(): void { } async function showStoredCssHotfixes(): Promise<void> { - const cachedCSS = await cache.get<string>('style-hotfixes:'); + const cachedCSS = await styleHotfixes.getCached(version); select('#hotfixes-field')!.textContent = isDevelopmentVersion() ? 'Hotfixes are not applied in the development version.' diff --git a/source/refined-github.ts b/source/refined-github.ts index 0235d210..89100161 100644 --- a/source/refined-github.ts +++ b/source/refined-github.ts @@ -175,7 +175,6 @@ import './features/rgh-feature-descriptions.js'; import './features/wait-for-attachments.js'; import './features/archive-forks-link.js'; import './features/link-to-changelog-file.js'; -import './features/rgh-sponsor-button.js'; import './features/rgh-linkify-features.js'; import './features/conversation-activity-filter.js'; import './features/select-all-notifications-shortcut.js'; diff --git a/tsconfig.json b/tsconfig.json index 27431088..886d8e70 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "target": "es2022", "module": "es2022", - "moduleResolution": "Node", "noUncheckedIndexedAccess": false, "noPropertyAccessFromIndexSignature": false, "lib": [ diff --git a/webpack.config.ts b/webpack.config.ts index 91d52a56..2c9fd70a 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -1,4 +1,4 @@ -/// <reference types="./source/globals" /> +/// <reference types="./source/globals.js" /> import path from 'node:path'; import SizePlugin from 'size-plugin'; @@ -18,7 +18,7 @@ const config: Configuration = { 'background', 'options', 'resolve-conflicts', - ].map(name => [name, `./${name}`])), + ].map(name => [name, `./${name}.js`])), context: path.resolve('source'), output: { path: path.resolve('distribution/assets'), @@ -71,8 +71,6 @@ const config: Configuration = { react: 'dom-chef', }, extensions: [ - '.tsx', - '.ts', '.js', ], extensionAlias: { |