@@ -759,6 +759,26 @@ function isGitHubFileNodeArray(value: unknown): value is Array<GitHubFileNode> {
759759 return Array . isArray ( value ) && value . every ( ( item ) => isGitHubFileNode ( item ) )
760760}
761761
762+ function isGitHubFile ( value : unknown ) : value is GitHubFile {
763+ if ( ! isRecord ( value ) ) {
764+ return false
765+ }
766+
767+ const links = value . _links
768+
769+ return (
770+ typeof value . name === 'string' &&
771+ typeof value . path === 'string' &&
772+ typeof value . type === 'string' &&
773+ isRecord ( links ) &&
774+ typeof links . self === 'string'
775+ )
776+ }
777+
778+ function isGitHubFileArray ( value : unknown ) : value is Array < GitHubFile > {
779+ return Array . isArray ( value ) && value . every ( ( item ) => isGitHubFile ( item ) )
780+ }
781+
762782function isGitHubTreeEntry ( value : unknown ) : value is GitHubTreeEntry {
763783 if ( typeof value !== 'object' || value === null ) {
764784 return false
@@ -1036,6 +1056,15 @@ function buildFileTreeFromRecursiveTree(
10361056
10371057const API_CONTENTS_MAX_DEPTH = 3
10381058
1059+ function encodeGitHubContentsPath ( path : string ) {
1060+ return removeLeadingSlash ( path )
1061+ . replace ( / \/ + $ / g, '' )
1062+ . split ( '/' )
1063+ . filter ( Boolean )
1064+ . map ( ( segment ) => encodeURIComponent ( segment ) )
1065+ . join ( '/' )
1066+ }
1067+
10391068export function fetchApiContents (
10401069 repoPair : string ,
10411070 branch : string ,
@@ -1182,11 +1211,98 @@ async function fetchApiContentsRemote(
11821211 branch : string ,
11831212 startingPath : string ,
11841213) : Promise < Array < GitHubFileNode > | null > {
1185- const tree = await fetchGitHubRecursiveTree ( repo , branch )
1214+ try {
1215+ const tree = await fetchGitHubRecursiveTree ( repo , branch )
1216+
1217+ if ( tree ) {
1218+ const fileTree = buildFileTreeFromRecursiveTree ( tree , startingPath )
1219+
1220+ if ( fileTree ) {
1221+ return fileTree
1222+ }
1223+ }
1224+ } catch ( error ) {
1225+ if ( ! isRecoverableGitHubContentError ( error ) ) {
1226+ throw error
1227+ }
1228+ }
1229+
1230+ return fetchApiContentsRemoteFromContentsApi ( repo , branch , startingPath )
1231+ }
1232+
1233+ async function fetchGitHubDirectoryContents (
1234+ repo : string ,
1235+ branch : string ,
1236+ directoryPath : string ,
1237+ ) {
1238+ const encodedPath = encodeGitHubContentsPath ( directoryPath )
1239+ const url = new URL (
1240+ `https://api.github.com/repos/${ repo } /contents/${ encodedPath } ` ,
1241+ )
1242+ url . searchParams . set ( 'ref' , branch )
1243+
1244+ const response = await fetchGitHubApiJson ( url . href )
1245+
1246+ if ( response === null ) {
1247+ return null
1248+ }
1249+
1250+ if ( ! isGitHubFileArray ( response ) ) {
1251+ throw new GitHubContentError (
1252+ 'invalid-response' ,
1253+ `Unexpected directory contents response for ${ repo } @${ branch } :${ directoryPath } ` ,
1254+ )
1255+ }
1256+
1257+ return response . filter ( ( file ) => file . type === 'dir' || file . type === 'file' )
1258+ }
1259+
1260+ async function fetchApiContentsRemoteFromContentsApi (
1261+ repo : string ,
1262+ branch : string ,
1263+ startingPath : string ,
1264+ ) : Promise < Array < GitHubFileNode > | null > {
1265+ const data = await fetchGitHubDirectoryContents ( repo , branch , startingPath )
11861266
1187- if ( ! tree ) {
1267+ if ( ! data || data . length === 0 ) {
11881268 return null
11891269 }
11901270
1191- return buildFileTreeFromRecursiveTree ( tree , startingPath )
1271+ async function buildFileTree (
1272+ nodes : Array < GitHubFile > ,
1273+ depth : number ,
1274+ parentPath : string ,
1275+ ) : Promise < Array < GitHubFileNode > > {
1276+ const result : Array < GitHubFileNode > = [ ]
1277+ const sortedNodes = sortApiContents ( nodes )
1278+
1279+ for ( const node of sortedNodes ) {
1280+ const file : GitHubFileNode = {
1281+ ...node ,
1282+ depth,
1283+ parentPath,
1284+ }
1285+
1286+ if ( file . type === 'dir' && depth <= API_CONTENTS_MAX_DEPTH ) {
1287+ const directoryFiles = await fetchGitHubDirectoryContents (
1288+ repo ,
1289+ branch ,
1290+ file . path ,
1291+ )
1292+ file . children = directoryFiles
1293+ ? await buildFileTree (
1294+ directoryFiles ,
1295+ depth + 1 ,
1296+ `${ parentPath } ${ file . path } /` ,
1297+ )
1298+ : [ ]
1299+ }
1300+
1301+ result . push ( file )
1302+ }
1303+
1304+ return result
1305+ }
1306+
1307+ return buildFileTree ( data , 0 , '' )
11921308}
0 commit comments