@@ -20,6 +20,35 @@ function exists(selector: string): boolean {
2020 return Boolean ( document . querySelector ( selector ) ) ;
2121}
2222
23+ /**
24+ * Waits for a detection to return true by repeatedly checking it on each animation frame.
25+ * Useful for DOM-based detections that need to wait for elements to appear.
26+ * @param detection - A detection function to check repeatedly
27+ * @returns A promise that resolves to the final result of the detection
28+ * @example
29+ * ```
30+ * import {utils} from 'github-url-detection';
31+ *
32+ * async function init() {
33+ * if (!await utils.waitFor(isOrganizationProfile)) {
34+ * return;
35+ * }
36+ * // Do something when on organization profile
37+ * }
38+ * ```
39+ */
40+ async function waitFor ( detection : ( ) => boolean ) : Promise < boolean > {
41+ // eslint-disable-next-line no-await-in-loop -- We need to wait on each frame
42+ while ( ! detection ( ) && document . readyState !== 'complete' ) {
43+ // eslint-disable-next-line no-await-in-loop
44+ await new Promise ( resolve => {
45+ requestAnimationFrame ( resolve ) ;
46+ } ) ;
47+ }
48+
49+ return detection ( ) ;
50+ }
51+
2352const combinedTestOnly = [ 'combinedTestOnly' ] ; // To be used only to skip tests of combined functions, i.e. isPageA() || isPageB()
2453
2554TEST: addTests ( '__urls_that_dont_match__' , [
@@ -68,9 +97,7 @@ TEST: addTests('isCompare', [
6897 'https://github.com/sindresorhus/refined-github/compare' ,
6998 'https://github.com/sindresorhus/refined-github/compare/' ,
7099 'https://github.com/sindresorhus/refined-github/compare/master...branch-name' ,
71- 'https://github.com/sindresorhus/refined-github/compare/master...branch-name?quick_pull=1' ,
72- 'https://github.com/sindresorhus/refined-github/compare/branch-1...branch-2?quick_pull=1' ,
73- 'https://github.com/sindresorhus/refined-github/compare/test-branch?quick_pull=1' ,
100+ 'isQuickPR' ,
74101] ) ;
75102
76103export const isCompareWikiPage = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => isRepoWiki ( url ) && getCleanPathname ( url ) . split ( '/' ) . slice ( 3 , 5 ) . includes ( '_compare' ) ;
@@ -79,17 +106,19 @@ TEST: addTests('isCompareWikiPage', [
79106 'https://github.com/brookhong/Surfingkeys/wiki/Color-Themes/_compare/8ebb46b1a12d16fc1af442b7df0ca13ca3bb34dc...80e51eeabe69b15a3f23880ecc36f800b71e6c6d' ,
80107] ) ;
81108
109+ /**
110+ * @deprecated Use `isHome` and/or `isFeed` instead
111+ */
82112export const isDashboard = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => ! isGist ( url ) && / ^ $ | ^ ( o r g s \/ [ ^ / ] + \/ ) ? d a s h b o a r d ( - f e e d ) ? ( \/ | $ ) / . test ( getCleanPathname ( url ) ) ;
83113TEST: addTests ( 'isDashboard' , [
84114 'https://github.com///' ,
85115 'https://github.com//' ,
86116 'https://github.com/' ,
87117 'https://github.com' ,
88- 'https://github.com/orgs/test /dashboard' ,
118+ 'https://github.com/orgs/refined-github /dashboard' ,
89119 'https://github.com/dashboard/index/2' ,
90120 'https://github.com//dashboard' ,
91121 'https://github.com/dashboard' ,
92- 'https://github.com/orgs/edit/dashboard' ,
93122 'https://github.big-corp.com/' ,
94123 'https://not-github.com/' ,
95124 'https://my-little-hub.com/' ,
@@ -102,6 +131,31 @@ TEST: addTests('isDashboard', [
102131 'https://github.com/dashboard-feed' ,
103132] ) ;
104133
134+ export const isHome = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => ! isGist ( url ) && / ^ $ | ^ d a s h b o a r d \/ ? $ / . test ( getCleanPathname ( url ) ) ;
135+ TEST: addTests ( 'isHome' , [
136+ 'https://github.com' ,
137+ 'https://github.com//dashboard' ,
138+ 'https://github.com///' ,
139+ 'https://github.com//' ,
140+ 'https://github.com/' ,
141+ 'https://github.com/dashboard' ,
142+ 'https://github.big-corp.com/' ,
143+ 'https://not-github.com/' ,
144+ 'https://my-little-hub.com/' ,
145+ 'https://github.com/?tab=repositories' , // Gotcha for `isUserProfileRepoTab`
146+ 'https://github.com/?tab=stars' , // Gotcha for `isUserProfileStarsTab`
147+ 'https://github.com/?tab=followers' , // Gotcha for `isUserProfileFollowersTab`
148+ 'https://github.com/?tab=following' , // Gotcha for `isUserProfileFollowingTab`
149+ 'https://github.com/?tab=overview' , // Gotcha for `isUserProfileMainTab`
150+ 'https://github.com?search=1' , // Gotcha for `isRepoTree`
151+ ] ) ;
152+
153+ export const isFeed = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => ! isGist ( url ) && / ^ ( f e e d | o r g s \/ [ ^ / ] + \/ d a s h b o a r d ) \/ ? $ / . test ( getCleanPathname ( url ) ) ;
154+ TEST: addTests ( 'isFeed' , [
155+ 'https://github.com/feed' ,
156+ 'https://github.com/orgs/refined-github/dashboard' ,
157+ ] ) ;
158+
105159export const isEnterprise = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => url . hostname !== 'github.com' && url . hostname !== 'gist.github.com' ;
106160TEST: addTests ( 'isEnterprise' , [
107161 'https://github.big-corp.com/' ,
@@ -204,7 +258,11 @@ TEST: addTests('isNotifications', [
204258
205259export const isOrganizationProfile = ( ) : boolean => exists ( 'meta[name="hovercard-subject-tag"][content^="organization"]' ) ;
206260
207- export const isOrganizationRepo = ( ) : boolean => exists ( '.AppHeader-context-full [data-hovercard-type="organization"]' ) ;
261+ export const isOrganizationRepo = ( ) : boolean => exists ( [
262+ 'qbsearch-input[data-current-repository][data-current-org]:not([data-current-repository=""], [data-current-org=""])' ,
263+ // TODO: Remove after June 2026
264+ '.AppHeader-context-full [data-hovercard-type="organization"]' ,
265+ ] . join ( ',' ) ) ;
208266
209267export const isTeamDiscussion = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => Boolean ( getOrg ( url ) ?. path . startsWith ( 'teams' ) ) ;
210268TEST: addTests ( 'isTeamDiscussion' , [
@@ -304,18 +362,20 @@ TEST: addTests('isPRFiles', [
304362 'https://github.com/refined-github/refined-github/pull/148/changes/1e27d7998afdd3608d9fc3bf95ccf27fa5010641..e1aba6febb3fe38aafd1137cff28b536eeeabe7e' ,
305363] ) ;
306364
307- export const isQuickPR = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => isCompare ( url ) && / [ ? & ] q u i c k _ p u l l = 1 ( & | $ ) / . test ( url . search ) ;
365+ export const isQuickPR = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => isCompare ( url ) && / [ ? & ] ( q u i c k _ p u l l | e x p a n d ) = 1 ( & | $ ) / . test ( url . search ) ;
308366TEST: addTests ( 'isQuickPR' , [
309367 'https://github.com/sindresorhus/refined-github/compare/master...branch-name?quick_pull=1' ,
310368 'https://github.com/sindresorhus/refined-github/compare/branch-1...branch-2?quick_pull=1' ,
311369 'https://github.com/sindresorhus/refined-github/compare/test-branch?quick_pull=1' ,
370+ 'https://github.com/refined-github/sandbox/compare/fregante-patch-2?expand=1' ,
371+ 'https://github.com/refined-github/sandbox/compare/default-a...fregante-patch-2?expand=1' ,
312372] ) ;
313373
314374const getStateLabel = ( ) : string | undefined => $ ( [
315375 '.State' , // Old view
316376 // React versions
317- '[class^="StateLabel"]' ,
318- '[data-testid="header-state "]' ,
377+ '[class^="StateLabel"]' , // TODO: Remove after July 2026
378+ '[class^="prc-StateLabel-StateLabel "]' ,
319379] . join ( ',' ) ) ?. textContent ?. trim ( ) ;
320380
321381export const isMergedPR = ( ) : boolean => getStateLabel ( ) === 'Merged' ;
@@ -409,9 +469,16 @@ TEST: addTests('isRepo', [
409469export const hasRepoHeader = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => isRepo ( url ) && ! isRepoSearch ( url ) ;
410470TEST: addTests ( 'hasRepoHeader' , combinedTestOnly ) ;
411471
412- // On empty repos, there's only isRepoHome; this element is found in `<head>`
413- export const isEmptyRepoRoot = ( ) : boolean => isRepoHome ( ) && ! exists ( 'link[rel="canonical"]' ) ;
472+ export const isEmptyRepoRoot = ( ) : boolean => isRepoHome ( ) && exists ( [
473+ // If you don't have write access
474+ '.blankslate-icon' ,
475+ // If you have write access
476+ '#empty-setup-clone-url' ,
477+ ] . join ( ',' ) ) ;
414478
479+ /**
480+ * @deprecated Doesn't work anymore. Use `isEmptyRepoRoot` or API instead.
481+ */
415482export const isEmptyRepo = ( ) : boolean => exists ( '[aria-label="Cannot fork because repository is empty."]' ) ;
416483
417484export const isPublicRepo = ( ) : boolean => exists ( 'meta[name="octolytics-dimension-repository_public"][content="true"]' ) ;
@@ -677,6 +744,16 @@ TEST: addTests('isGistRevision', [
677744 'https://gist.github.com/kidonng/0d16c7f17045f486751fad1b602204a0/revisions' ,
678745] ) ;
679746
747+ export const isGistFile = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => {
748+ const pathname = getCleanGistPathname ( url ) ;
749+ return pathname ?. replace ( / [ ^ / ] / g, '' ) . length === 1 ;
750+ } ;
751+
752+ TEST: addTests ( 'isGistFile' , [
753+ 'https://gist.github.com/fregante/2205329b71218fa2c1d3' ,
754+ 'https://gist.github.com/sindresorhus/0ea3c2845718a0a0f0beb579ff14f064' ,
755+ ] ) ;
756+
680757export const isTrending = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => url . pathname === '/trending' || url . pathname . startsWith ( '/trending/' ) ;
681758TEST: addTests ( 'isTrending' , [
682759 'https://github.com/trending' ,
@@ -728,7 +805,7 @@ TEST: addTests('isGistProfile', [
728805
729806export const isUserProfile = ( ) : boolean => isProfile ( ) && ! isOrganizationProfile ( ) ;
730807
731- export const isPrivateUserProfile = ( ) : boolean => isUserProfile ( ) && ! exists ( '.UnderlineNav-item[href$="tab=stars"] ' ) ;
808+ export const isPrivateUserProfile = ( ) : boolean => isUserProfile ( ) && exists ( '#user-private-profile-frame ' ) ;
732809
733810export const isUserProfileMainTab = ( ) : boolean =>
734811 isUserProfile ( )
@@ -860,7 +937,20 @@ TEST: addTests('isRepositoryActions', [
860937
861938export const isUserTheOrganizationOwner = ( ) : boolean => isOrganizationProfile ( ) && exists ( '[aria-label="Organization"] [data-tab-item="org-header-settings-tab"]' ) ;
862939
863- export const canUserAdminRepo = ( ) : boolean => isRepo ( ) && exists ( '.reponav-item[href$="/settings"], [data-tab-item$="settings-tab"]' ) ;
940+ /**
941+ * @deprecated Use canUserAccessRepoSettings or API instead.
942+ */
943+ export const canUserAdminRepo = ( ) : boolean => {
944+ const repo = getRepo ( ) ;
945+ return Boolean ( repo && exists ( `:is(${ [
946+ '.GlobalNav' ,
947+ // Remove after June 2026
948+ '.js-repo-nav' ,
949+ ] . join ( ',' ) } ) a[href="/${ repo . nameWithOwner } /settings"]`) ) ;
950+ } ;
951+
952+ // eslint-disable-next-line @typescript-eslint/no-deprecated
953+ export const canUserAccessRepoSettings = canUserAdminRepo ;
864954
865955export const isNewRepo = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => ! isGist ( url ) && ( url . pathname === '/new' || / ^ o r g a n i z a t i o n s \/ [ ^ / ] + \/ r e p o s i t o r i e s \/ n e w $ / . test ( getCleanPathname ( url ) ) ) ;
866956TEST: addTests ( 'isNewRepo' , [
@@ -968,4 +1058,5 @@ export const utils = {
9681058 getCleanGistPathname,
9691059 getRepositoryInfo : getRepo ,
9701060 parseRepoExplorerTitle,
1061+ waitFor,
9711062} ;
0 commit comments