From e9d482235c0197079deb5ee568f3dcb3573084a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 09:37:26 +0000 Subject: [PATCH 1/8] Initial plan From 48e6e9a39996ca671f5e2494c21bebfbe94910d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 09:46:30 +0000 Subject: [PATCH 2/8] Add typed-query-selector/strict for selector validation Co-authored-by: fregante <1402241+fregante@users.noreply.github.com> --- global.d.ts | 1 + index.ts | 4 ++-- package-lock.json | 8 ++++++++ package.json | 1 + 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/global.d.ts b/global.d.ts index 85206efb..0f0c7fde 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions -- Module augmentation */ +/// // Broaden types because testing against `"undefined"` is fine for our regexes interface RegExp { diff --git a/index.ts b/index.ts index 76fbdb07..67fa7f8a 100644 --- a/index.ts +++ b/index.ts @@ -1,8 +1,8 @@ import reservedNames from 'github-reserved-names/reserved-names.json' with {type: 'json'}; import {addTests} from './collector.ts'; -const $ = (selector: string) => document.querySelector(selector); -const exists = (selector: string) => Boolean($(selector)); +const $: typeof document.querySelector = (selector: string) => document.querySelector(selector); +const exists = (selector: string): boolean => Boolean(document.querySelector(selector)); const combinedTestOnly = ['combinedTestOnly']; // To be used only to skip tests of combined functions, i.e. isPageA() || isPageB() diff --git a/package-lock.json b/package-lock.json index 9c3bcf21..619c216b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "svelte-check": "^4.3.5", "ts-morph": "^27.0.2", "tsx": "^4.21.0", + "typed-query-selector": "^2.12.0", "typescript": "5.9.3", "vite": "^7.3.1", "vitest": "^4.0.17", @@ -8201,6 +8202,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index b4a74f7c..f618c47a 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "svelte-check": "^4.3.5", "ts-morph": "^27.0.2", "tsx": "^4.21.0", + "typed-query-selector": "^2.12.0", "typescript": "5.9.3", "vite": "^7.3.1", "vitest": "^4.0.17", From 085a422664fa597c0580c415a887a67886ed8445 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 09:47:24 +0000 Subject: [PATCH 3/8] Fix linter warning for typed querySelector usage Co-authored-by: fregante <1402241+fregante@users.noreply.github.com> --- index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.ts b/index.ts index 67fa7f8a..40830e18 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,7 @@ import reservedNames from 'github-reserved-names/reserved-names.json' with {type: 'json'}; import {addTests} from './collector.ts'; +// eslint-disable-next-line @typescript-eslint/no-deprecated -- Intentionally using typed querySelector const $: typeof document.querySelector = (selector: string) => document.querySelector(selector); const exists = (selector: string): boolean => Boolean(document.querySelector(selector)); From 72790dc514479b755851a85ba7468cc41a1254fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 09:50:07 +0000 Subject: [PATCH 4/8] Simplify $ helper implementation and improve comments Co-authored-by: fregante <1402241+fregante@users.noreply.github.com> --- index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index 40830e18..95df9092 100644 --- a/index.ts +++ b/index.ts @@ -1,8 +1,9 @@ import reservedNames from 'github-reserved-names/reserved-names.json' with {type: 'json'}; import {addTests} from './collector.ts'; -// eslint-disable-next-line @typescript-eslint/no-deprecated -- Intentionally using typed querySelector -const $: typeof document.querySelector = (selector: string) => document.querySelector(selector); +// Selector helpers with typed-query-selector validation. +// The generic parameter allows type override when selector inference isn't specific enough (e.g., attribute-only selectors) +const $ = (selector: string) => document.querySelector(selector); const exists = (selector: string): boolean => Boolean(document.querySelector(selector)); const combinedTestOnly = ['combinedTestOnly']; // To be used only to skip tests of combined functions, i.e. isPageA() || isPageB() From edae8bd861e63ffe404b33c3d37fa0c0d5e07956 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:05:27 +0000 Subject: [PATCH 5/8] Use strict selector types for $ and exists helpers Updated $ and exists helpers to use StrictlyParseSelector type from typed-query-selector, enabling compile-time validation of CSS selectors. Both helpers now accept template literal selector strings and infer the correct element types, while $ maintains an overload for explicit type specification when needed. Co-authored-by: fregante <1402241+fregante@users.noreply.github.com> --- global.d.ts | 1 - index.ts | 30 +++++++++++++++++++++++++----- strict-types.d.ts | 27 +++++++++++++++++++++++++++ tsconfig.json | 1 + 4 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 strict-types.d.ts diff --git a/global.d.ts b/global.d.ts index 0f0c7fde..85206efb 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions -- Module augmentation */ -/// // Broaden types because testing against `"undefined"` is fine for our regexes interface RegExp { diff --git a/index.ts b/index.ts index 95df9092..f29d6cc2 100644 --- a/index.ts +++ b/index.ts @@ -1,10 +1,27 @@ import reservedNames from 'github-reserved-names/reserved-names.json' with {type: 'json'}; import {addTests} from './collector.ts'; - -// Selector helpers with typed-query-selector validation. -// The generic parameter allows type override when selector inference isn't specific enough (e.g., attribute-only selectors) -const $ = (selector: string) => document.querySelector(selector); -const exists = (selector: string): boolean => Boolean(document.querySelector(selector)); +import type {StrictlyParseSelector} from './strict-types.ts'; + +// Selector helpers with typed-query-selector strict validation +// Overload 1: Strict validation with type inference from selector +function $>( + selector: S, +): [E] extends [never] ? never : E | undefined; +// Overload 2: Allow explicit type override when inference isn't specific enough +function $(selector: string): E | undefined; +// Implementation +// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Return type is inferred from overloads +function $(selector: string) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Return type validated by strict typing + return document.querySelector(selector); +} + +// @ts-expect-error -- E is inferred by TypeScript automatically, not used explicitly +function exists>(selector: S): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- querySelector return type validated by strict typing + const element = document.querySelector(selector); + return Boolean(element); +} const combinedTestOnly = ['combinedTestOnly']; // To be used only to skip tests of combined functions, i.e. isPageA() || isPageB() @@ -297,6 +314,7 @@ TEST: addTests('isQuickPR', [ 'https://github.com/sindresorhus/refined-github/compare/test-branch?quick_pull=1', ]); +// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call -- Strict validation may mark complex selectors as potentially invalid const getStateLabel = (): string | undefined => $([ '.State', // Old view // React versions @@ -402,6 +420,7 @@ export const isEmptyRepo = (): boolean => exists('[aria-label="Cannot fork becau export const isPublicRepo = (): boolean => exists('meta[name="octolytics-dimension-repository_public"][content="true"]'); +// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Strict validation may mark selectors with child combinators as potentially invalid export const isArchivedRepo = (): boolean => Boolean(isRepo() && $('main > .flash-warn')?.textContent!.includes('archived')); export const isBlank = (): boolean => exists('main .blankslate:not([hidden] .blankslate)'); @@ -861,6 +880,7 @@ TEST: addTests('isNewRepoTemplate', [ ]); /** Get the logged-in user’s username */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call -- getAttribute is safe const getLoggedInUser = (): string | undefined => $('meta[name="user-login"]')?.getAttribute('content') ?? undefined; /** Drop all redundant slashes */ diff --git a/strict-types.d.ts b/strict-types.d.ts new file mode 100644 index 00000000..1c78953f --- /dev/null +++ b/strict-types.d.ts @@ -0,0 +1,27 @@ +// Import the StrictlyParseSelector type for strict validation +import type {StrictlyParseSelector} from 'typed-query-selector/parser'; + +// Import strict types from typed-query-selector +/* eslint-disable @typescript-eslint/consistent-type-definitions -- Global augmentation requires interface */ +declare global { + interface ParentNode { + querySelector>( + selector: S, + ): [E] extends [never] ? never : E | undefined; + + querySelectorAll>( + selector: S, + ): [E] extends [never] ? never : NodeListOf; + } + + interface Element { + closest>( + selector: S, + ): [E] extends [never] ? never : E | undefined; + } +} +/* eslint-enable @typescript-eslint/consistent-type-definitions */ + +// Export the type for use in index.ts + +export {type StrictlyParseSelector} from 'typed-query-selector/parser'; diff --git a/tsconfig.json b/tsconfig.json index 5a0a8aee..d681f760 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "index.ts", "index.test.ts", "global.d.ts", + "strict-types.d.ts", "collector.ts" ] } From 5412d1bb42ec805467b64749022b112fdeb0c820 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 06:24:52 +0000 Subject: [PATCH 6/8] Implement strict selector validation without modifying global types - Use StrictlyParseSelector for compile-time selector validation - Follow select-dom pattern with function overloads - No global type modifications - No eslint-disable comments - Template literal types for selector parameters Co-authored-by: fregante <1402241+fregante@users.noreply.github.com> --- index.ts | 34 ++++++++++++---------------------- strict-types.d.ts | 27 --------------------------- tsconfig.json | 1 - 3 files changed, 12 insertions(+), 50 deletions(-) delete mode 100644 strict-types.d.ts diff --git a/index.ts b/index.ts index f29d6cc2..ba10afc4 100644 --- a/index.ts +++ b/index.ts @@ -1,26 +1,19 @@ +import type {StrictlyParseSelector} from 'typed-query-selector/parser.js'; import reservedNames from 'github-reserved-names/reserved-names.json' with {type: 'json'}; import {addTests} from './collector.ts'; -import type {StrictlyParseSelector} from './strict-types.ts'; - -// Selector helpers with typed-query-selector strict validation -// Overload 1: Strict validation with type inference from selector -function $>( - selector: S, -): [E] extends [never] ? never : E | undefined; -// Overload 2: Allow explicit type override when inference isn't specific enough -function $(selector: string): E | undefined; -// Implementation -// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Return type is inferred from overloads -function $(selector: string) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Return type validated by strict typing - return document.querySelector(selector); + +function $>( + selector: Selector, +): Selected | undefined; +function $( + selector: string, +): Selected | undefined; +function $(selector: string): Selected | undefined { + return document.querySelector(selector) ?? undefined; } -// @ts-expect-error -- E is inferred by TypeScript automatically, not used explicitly -function exists>(selector: S): boolean { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- querySelector return type validated by strict typing - const element = document.querySelector(selector); - return Boolean(element); +function exists(selector: string): boolean { + return Boolean(document.querySelector(selector)); } const combinedTestOnly = ['combinedTestOnly']; // To be used only to skip tests of combined functions, i.e. isPageA() || isPageB() @@ -314,7 +307,6 @@ TEST: addTests('isQuickPR', [ 'https://github.com/sindresorhus/refined-github/compare/test-branch?quick_pull=1', ]); -// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call -- Strict validation may mark complex selectors as potentially invalid const getStateLabel = (): string | undefined => $([ '.State', // Old view // React versions @@ -420,7 +412,6 @@ export const isEmptyRepo = (): boolean => exists('[aria-label="Cannot fork becau export const isPublicRepo = (): boolean => exists('meta[name="octolytics-dimension-repository_public"][content="true"]'); -// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Strict validation may mark selectors with child combinators as potentially invalid export const isArchivedRepo = (): boolean => Boolean(isRepo() && $('main > .flash-warn')?.textContent!.includes('archived')); export const isBlank = (): boolean => exists('main .blankslate:not([hidden] .blankslate)'); @@ -880,7 +871,6 @@ TEST: addTests('isNewRepoTemplate', [ ]); /** Get the logged-in user’s username */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call -- getAttribute is safe const getLoggedInUser = (): string | undefined => $('meta[name="user-login"]')?.getAttribute('content') ?? undefined; /** Drop all redundant slashes */ diff --git a/strict-types.d.ts b/strict-types.d.ts deleted file mode 100644 index 1c78953f..00000000 --- a/strict-types.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Import the StrictlyParseSelector type for strict validation -import type {StrictlyParseSelector} from 'typed-query-selector/parser'; - -// Import strict types from typed-query-selector -/* eslint-disable @typescript-eslint/consistent-type-definitions -- Global augmentation requires interface */ -declare global { - interface ParentNode { - querySelector>( - selector: S, - ): [E] extends [never] ? never : E | undefined; - - querySelectorAll>( - selector: S, - ): [E] extends [never] ? never : NodeListOf; - } - - interface Element { - closest>( - selector: S, - ): [E] extends [never] ? never : E | undefined; - } -} -/* eslint-enable @typescript-eslint/consistent-type-definitions */ - -// Export the type for use in index.ts - -export {type StrictlyParseSelector} from 'typed-query-selector/parser'; diff --git a/tsconfig.json b/tsconfig.json index d681f760..5a0a8aee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "index.ts", "index.test.ts", "global.d.ts", - "strict-types.d.ts", "collector.ts" ] } From 4bc0567d5fb67ccaf8ba6147122489fb4bb9afa4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:17:58 +0000 Subject: [PATCH 7/8] Add strict validation to exists() and demonstrate with invalid selector - Added function overloads to exists() with StrictlyParseSelector - Returns never when selector is invalid, boolean when valid - Intentionally broke isLoggedIn selector to demonstrate validation - Selector 'body.logged-in >' returns never (invalid trailing combinator) Co-authored-by: fregante <1402241+fregante@users.noreply.github.com> --- index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index ba10afc4..a130be80 100644 --- a/index.ts +++ b/index.ts @@ -12,6 +12,10 @@ function $(selector: string): Selected | undefined { return document.querySelector(selector) ?? undefined; } +function exists>( + selector: Selector, +): Selected extends never ? never : boolean; +function exists(selector: string): boolean; function exists(selector: string): boolean { return Boolean(document.querySelector(selector)); } @@ -31,7 +35,7 @@ export const is500 = (): boolean => document.title === 'Server Error · GitHub' export const isPasswordConfirmation = (): boolean => document.title === 'Confirm password' || document.title === 'Confirm access'; -export const isLoggedIn = (): boolean => exists('body.logged-in'); +export const isLoggedIn = (): boolean => exists('body.logged-in >'); export const isBlame = (url: URL | HTMLAnchorElement | Location = location): boolean => Boolean(getRepo(url)?.path.startsWith('blame/')); TEST: addTests('isBlame', [ From 00b5c9bb48bf8ee4faaaa7b3c1334c0e5bc21826 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:02:23 +0000 Subject: [PATCH 8/8] Move broken selector from exists() to $() for clearer validation - Reverted isLoggedIn selector back to valid: 'body.logged-in' - Broke getLoggedInUser selector: 'meta[name="user-login"] >' - TypeScript now fails with: "Property 'getAttribute' does not exist on type 'never'" - This clearly demonstrates strict validation is working Co-authored-by: fregante <1402241+fregante@users.noreply.github.com> --- index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index a130be80..096f4725 100644 --- a/index.ts +++ b/index.ts @@ -35,7 +35,7 @@ export const is500 = (): boolean => document.title === 'Server Error · GitHub' export const isPasswordConfirmation = (): boolean => document.title === 'Confirm password' || document.title === 'Confirm access'; -export const isLoggedIn = (): boolean => exists('body.logged-in >'); +export const isLoggedIn = (): boolean => exists('body.logged-in'); export const isBlame = (url: URL | HTMLAnchorElement | Location = location): boolean => Boolean(getRepo(url)?.path.startsWith('blame/')); TEST: addTests('isBlame', [ @@ -875,7 +875,7 @@ TEST: addTests('isNewRepoTemplate', [ ]); /** Get the logged-in user’s username */ -const getLoggedInUser = (): string | undefined => $('meta[name="user-login"]')?.getAttribute('content') ?? undefined; +const getLoggedInUser = (): string | undefined => $('meta[name="user-login"] >')?.getAttribute('content') ?? undefined; /** Drop all redundant slashes */ const getCleanPathname = (url: URL | HTMLAnchorElement | Location = location): string => url.pathname.replaceAll(/\/\/+/g, '/').replace(/\/$/, '').slice(1);