1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
export interface GitHubTreeItem {
name: string;
path: string;
contentType: "directory" | "file";
}
export interface GitHubTree {
path: string;
branch: string;
items: GitHubTreeItem[];
repo: {
name: string;
owner: string;
isPublic: boolean;
};
}
export async function getGitHubTree(
owner: string,
repo: string,
branch: string,
path: string,
): Promise<GitHubTree> {
const url = `https://github.com/${owner}/${repo}/tree/${branch}/${path}`;
// GitHub throttles/blocks requests without realistic browser headers.
// These headers make the request appear as a standard browser visit.
const response = await fetch(url, {
headers: {
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
Accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
},
});
const html = await response.text();
// GitHub uses different formats for root vs subdirectories.
let payload;
// Try root format first.
const rootFormatMatches = [
...html.matchAll(
/<script type="application\/json" data-target="react-partial\.embeddedData">([^<]+)<\/script>/g,
),
];
for (const match of rootFormatMatches) {
try {
const data = JSON.parse(match[1]);
if (data.props?.initialPayload?.tree) {
payload = data.props.initialPayload;
break;
}
} catch (e) {
continue;
}
}
// Try subdirectory format.
if (!payload) {
const dirFormatMatch = html.match(
/<script type="application\/json" data-target="react-app\.embeddedData">([^<]+)<\/script>/,
);
if (dirFormatMatch) {
try {
const data = JSON.parse(dirFormatMatch[1]);
if (data.payload?.tree) {
payload = data.payload;
}
} catch (e) {
// Skip invalid JSON
}
}
}
if (!payload) {
throw new Error("Could not find tree data in embedded JSON");
}
return {
path: payload.path,
branch: payload.refInfo.name,
items: payload.tree.items,
repo: {
name: payload.repo.name,
owner: payload.repo.ownerLogin,
isPublic: payload.repo.public,
},
};
}
|