repos / neovimcraft

website that makes it easy to find neovim plugins
git clone https://github.com/neurosnap/neovimcraft.git

neovimcraft / src
Eric Bower · 17 Apr 23

github.ts

  1import type { FetchRepoProps, Resp } from "./types.ts";
  2
  3import { encode } from "./deps.ts";
  4
  5const accessToken = Deno.env.get("GITHUB_ACCESS_TOKEN") || "";
  6const accessUsername = Deno.env.get("GITHUB_USERNAME") || "";
  7export const ghToken = encode(`${accessUsername}:${accessToken}`);
  8
  9function delay(ms: number): Promise<void> {
 10  return new Promise((resolve) => setTimeout(() => resolve(), ms));
 11}
 12
 13const nextRe = new RegExp(/\<([^<]+)\>; rel="next"/);
 14
 15async function githubApi<D = any>(
 16  endpoint: string,
 17  token: string,
 18): Promise<Resp<D>> {
 19  const url = `https://api.github.com${endpoint}`;
 20  console.log(`Fetching ${url}`);
 21  const res = await fetch(url, {
 22    headers: { Authorization: `Basic ${token}` },
 23  });
 24
 25  const rateLimitRemaining = parseInt(
 26    res.headers.get("X-RateLimit-Remaining") || "0",
 27  );
 28  // this value is in seconds, not ms
 29  const rateLimitReset = parseInt(res.headers.get("X-RateLimit-Reset") || "0");
 30  console.log(`rate limit remaining: ${rateLimitRemaining}`);
 31  if (rateLimitRemaining === 1 || rateLimitRemaining === 0) {
 32    const now = Date.now();
 33    const RESET_BUFFER = 500;
 34    const wait = (rateLimitReset * 1000) + RESET_BUFFER - now;
 35    // ms -> s -> min
 36    const mins = Math.ceil(wait / 1000 / 60);
 37    console.log(
 38      `Hit github rate limit, waiting ${mins} mins`,
 39    );
 40    await delay(wait);
 41  }
 42
 43  let data = null;
 44  try {
 45    data = await res.json();
 46  } catch {
 47    return {
 48      ok: false,
 49      data: {
 50        status: res.status,
 51        error: new Error(`JSON parsing error [${url}]`),
 52      },
 53    };
 54  }
 55
 56  if (res.ok) {
 57    // pagination
 58    let next = "";
 59    const link = res.headers.get("link");
 60    if (link) {
 61      const paginated = nextRe.exec(link || "");
 62      if (paginated && paginated.length > 1) {
 63        next = paginated[1];
 64      }
 65    }
 66
 67    return {
 68      ok: true,
 69      next,
 70      data,
 71    };
 72  }
 73
 74  return {
 75    ok: false,
 76    data: {
 77      status: res.status,
 78      error: new Error(`Could not load [${url}]`),
 79    },
 80  };
 81}
 82
 83async function fetchReadme({
 84  username,
 85  repo,
 86  token,
 87}: FetchRepoProps): Promise<Resp<string>> {
 88  const result = await githubApi(`/repos/${username}/${repo}/readme`, token);
 89  if (!result.ok) {
 90    return {
 91      ok: false,
 92      data: result.data as any,
 93    };
 94  }
 95
 96  const url = result.data.download_url;
 97  console.log(`Fetching ${url}`);
 98  const readme = await fetch(url);
 99  const data = await readme.text();
100  return {
101    ok: true,
102    data,
103  };
104}
105
106interface RepoData {
107  name: string;
108  html_url: string;
109  homepage: string;
110  default_branch: string;
111  open_issues_count: number;
112  watchers_count: number;
113  forks: number;
114  stargazers_count: number;
115  subscribers_count: number;
116  network_count: number;
117  description: string;
118  created_at: string;
119  updated_at: string;
120}
121
122async function fetchRepo({
123  username,
124  repo,
125  token,
126}: FetchRepoProps): Promise<Resp<RepoData>> {
127  const result = await githubApi(`/repos/${username}/${repo}`, token);
128  return result;
129}
130
131interface BranchData {
132  commit: {
133    commit: {
134      committer: {
135        date: string;
136      };
137    };
138  };
139}
140
141async function fetchBranch({
142  username,
143  repo,
144  branch,
145  token,
146}: FetchRepoProps & { branch: string }): Promise<Resp<BranchData>> {
147  const result = await githubApi(
148    `/repos/${username}/${repo}/branches/${branch}`,
149    token,
150  );
151  return result;
152}
153
154interface GithubData {
155  readme: string;
156  repo: RepoData;
157  branch: Resp<BranchData>;
158}
159
160export async function fetchGithubData(
161  props: FetchRepoProps,
162): Promise<Resp<GithubData>> {
163  const repo = await fetchRepo(props);
164  if (repo.ok === false) {
165    console.log(`${repo.data.status}: ${repo.data.error.message}`);
166    return repo;
167  }
168
169  const branch = await fetchBranch({
170    ...props,
171    branch: repo.data.default_branch,
172  });
173  if (branch.ok === false) {
174    console.log(`${branch.data.status}: ${branch.data.error.message}`);
175  }
176
177  const readme = await fetchReadme({
178    username: props.username,
179    repo: props.repo,
180    token: props.token,
181  });
182  if (readme.ok === false) {
183    console.log(`${readme.data.status}: ${readme.data.error.message}`);
184  }
185
186  return {
187    ok: true,
188    data: {
189      readme: readme.ok ? readme.data : "",
190      repo: repo.data,
191      branch,
192    },
193  };
194}
195
196type TopicRepo = RepoData & { full_name: string; topics: string[] };
197
198export async function fetchTopics(topic: string, token: string) {
199  const items: TopicRepo[] = [];
200  let next = `/search/repositories?q=${topic}&per_page=25`;
201  // limit the number of pages
202  // let count = 0;
203
204  while (next) {
205    const result = await githubApi<{ items: TopicRepo[] }>(
206      next,
207      token,
208    );
209
210    next = "";
211
212    if (result.ok) {
213      items.push(...result.data.items);
214
215      /* count += 1;
216      if (result.next && count < 3) {
217        next = result.next;
218        next = next.replace('https://api.github.com', '');
219      } */
220    }
221  }
222
223  return items;
224}