@@ -2,6 +2,14 @@ import { normalizeSearchName, parseGitHubRepository } from "./library-id.js";
22import type { DiscoveredLibrary , LibraryRef , LocalContext7Config } from "./types.js" ;
33
44type JsonObject = Record < string , unknown > ;
5+ const DOCUMENTATION_QUALIFIERS = new Set ( [
6+ "docs" ,
7+ "documentation" ,
8+ "manual" ,
9+ "reference" ,
10+ "references" ,
11+ ] ) ;
12+ const MAX_GITHUB_SEARCH_CANDIDATES = 3 ;
513
614function asObject ( value : unknown ) : JsonObject | undefined {
715 return value && typeof value === "object" ? ( value as JsonObject ) : undefined ;
@@ -17,6 +25,48 @@ function repositoryFromValue(value: unknown): LibraryRef | undefined {
1725 return object ? parseGitHubRepository ( asString ( object . url ) ?? "" ) : undefined ;
1826}
1927
28+ function discoveryNames ( name : string ) : string [ ] {
29+ const original = name . trim ( ) ;
30+ const words = original . split ( / \s + / ) ;
31+ while ( words . length > 1 && DOCUMENTATION_QUALIFIERS . has ( ( words . at ( - 1 ) ?? "" ) . toLowerCase ( ) ) ) {
32+ words . pop ( ) ;
33+ }
34+ const withoutQualifier = words . join ( " " ) ;
35+ return withoutQualifier && withoutQualifier !== original
36+ ? [ original , withoutQualifier ]
37+ : [ original ] ;
38+ }
39+
40+ function usableGitHubRepository ( value : JsonObject | undefined ) : boolean {
41+ if ( ! value || value . archived === true || value . disabled === true ) return false ;
42+ return typeof value . size !== "number" || value . size > 0 ;
43+ }
44+
45+ function discoveredFromGitHub ( value : JsonObject | undefined ) : DiscoveredLibrary | undefined {
46+ if ( ! usableGitHubRepository ( value ) ) return undefined ;
47+ const fullName = asString ( value ?. full_name ) ;
48+ const ref = fullName ? parseGitHubRepository ( fullName ) : undefined ;
49+ const defaultBranch = asString ( value ?. default_branch ) ;
50+ if ( ! ref || ! defaultBranch ) return undefined ;
51+ return {
52+ ref,
53+ title : asString ( value ?. name ) ,
54+ description : asString ( value ?. description ) ,
55+ stars : typeof value ?. stargazers_count === "number" ? value . stargazers_count : undefined ,
56+ defaultBranch,
57+ } ;
58+ }
59+
60+ function uniqueCandidates ( candidates : DiscoveredLibrary [ ] ) : DiscoveredLibrary [ ] {
61+ const seen = new Set < string > ( ) ;
62+ return candidates . filter ( ( candidate ) => {
63+ const key = candidate . ref . id . toLowerCase ( ) ;
64+ if ( seen . has ( key ) ) return false ;
65+ seen . add ( key ) ;
66+ return true ;
67+ } ) ;
68+ }
69+
2070export class LibraryDiscovery {
2171 constructor ( private readonly config : LocalContext7Config ) { }
2272
@@ -53,6 +103,19 @@ export class LibraryDiscovery {
53103 }
54104 }
55105
106+ private async verifiedGitHubRepository ( ref : LibraryRef ) : Promise < DiscoveredLibrary | undefined > {
107+ const headers : Record < string , string > = { } ;
108+ if ( this . config . githubToken ) headers . Authorization = `Bearer ${ this . config . githubToken } ` ;
109+ try {
110+ const data = asObject (
111+ await this . fetchJson ( `https://api.github.com/repos/${ ref . owner } /${ ref . repo } ` , headers )
112+ ) ;
113+ return discoveredFromGitHub ( data ) ;
114+ } catch {
115+ return undefined ;
116+ }
117+ }
118+
56119 private async fromNpm ( name : string ) : Promise < DiscoveredLibrary | undefined > {
57120 try {
58121 const data = asObject (
@@ -110,7 +173,18 @@ export class LibraryDiscovery {
110173 }
111174 }
112175
113- private async fromGitHubSearch ( name : string ) : Promise < DiscoveredLibrary | undefined > {
176+ private async fromOfficialDocumentationRepository (
177+ originalName : string ,
178+ searchName : string
179+ ) : Promise < DiscoveredLibrary | undefined > {
180+ if ( originalName . trim ( ) === searchName . trim ( ) ) return undefined ;
181+ const owner = searchName . trim ( ) . split ( / \s + / , 1 ) [ 0 ] ;
182+ if ( ! owner || ! / ^ [ A - Z a - z 0 - 9 _ . - ] + $ / . test ( owner ) ) return undefined ;
183+ const ref = parseGitHubRepository ( `/${ owner } /docs` ) ;
184+ return ref ? this . verifiedGitHubRepository ( ref ) : undefined ;
185+ }
186+
187+ private async fromGitHubSearch ( name : string ) : Promise < DiscoveredLibrary [ ] > {
114188 const headers : Record < string , string > = { } ;
115189 if ( this . config . githubToken ) headers . Authorization = `Bearer ${ this . config . githubToken } ` ;
116190 try {
@@ -122,32 +196,43 @@ export class LibraryDiscovery {
122196 ) ;
123197 const items = Array . isArray ( root ?. items ) ? root . items . map ( asObject ) . filter ( Boolean ) : [ ] ;
124198 const normalized = normalizeSearchName ( name ) ;
125- const exact = items . find (
126- ( item ) => normalizeSearchName ( asString ( item ?. name ) ?? "" ) === normalized
127- ) ;
128- const fullName = asString ( exact ?. full_name ) ;
129- const ref = fullName ? parseGitHubRepository ( fullName ) : undefined ;
130- if ( ! ref ) return undefined ;
131- return {
132- ref,
133- title : asString ( exact ?. name ) ,
134- description : asString ( exact ?. description ) ,
135- stars : typeof exact ?. stargazers_count === "number" ? exact . stargazers_count : undefined ,
136- defaultBranch : asString ( exact ?. default_branch ) ,
137- } ;
199+ return items
200+ . filter ( ( item ) => normalizeSearchName ( asString ( item ?. name ) ?? "" ) === normalized )
201+ . map ( discoveredFromGitHub )
202+ . filter ( ( candidate ) : candidate is DiscoveredLibrary => Boolean ( candidate ) )
203+ . sort ( ( left , right ) => ( right . stars ?? 0 ) - ( left . stars ?? 0 ) )
204+ . slice ( 0 , MAX_GITHUB_SEARCH_CANDIDATES ) ;
138205 } catch {
139- return undefined ;
206+ return [ ] ;
140207 }
141208 }
142209
143- async discover ( name : string ) : Promise < DiscoveredLibrary | undefined > {
210+ async discoverCandidates ( name : string ) : Promise < DiscoveredLibrary [ ] > {
144211 const direct = parseGitHubRepository ( name ) ;
145- if ( direct ) return this . decorate ( direct ) ;
212+ if ( direct ) return [ await this . decorate ( direct ) ] ;
146213
147- for ( const resolver of [ this . fromNpm , this . fromPyPi , this . fromCrates , this . fromGitHubSearch ] ) {
148- const result = await resolver . call ( this , name ) ;
149- if ( result ) return result ;
150- }
151- return undefined ;
214+ const names = discoveryNames ( name ) ;
215+ const registryCandidates = await Promise . all (
216+ names . flatMap ( ( candidateName ) =>
217+ [ this . fromNpm , this . fromPyPi , this . fromCrates ] . map ( ( resolver ) =>
218+ resolver . call ( this , candidateName )
219+ )
220+ )
221+ ) ;
222+ const [ officialDocumentation , ...githubCandidates ] = await Promise . all ( [
223+ this . fromOfficialDocumentationRepository ( names [ 0 ] ! , names . at ( - 1 ) ! ) ,
224+ ...names . map ( ( candidateName ) => this . fromGitHubSearch ( candidateName ) ) ,
225+ ] ) ;
226+ return uniqueCandidates ( [
227+ ...registryCandidates . filter ( ( candidate ) : candidate is DiscoveredLibrary =>
228+ Boolean ( candidate )
229+ ) ,
230+ ...( officialDocumentation ? [ officialDocumentation ] : [ ] ) ,
231+ ...githubCandidates . flat ( ) ,
232+ ] ) ;
233+ }
234+
235+ async discover ( name : string ) : Promise < DiscoveredLibrary | undefined > {
236+ return ( await this . discoverCandidates ( name ) ) [ 0 ] ;
152237 }
153238}
0 commit comments