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);