import { createWriteStream, promises } from "fs"; import got from "got"; import { tmpdir } from "os"; import { join } from "path"; import { Stream } from "stream"; import tar from "tar"; import { promisify } from "util"; import { makeDir } from "./make-dir"; import { CommunityProjectConfig } from "./types"; export type RepoInfo = { username: string; name: string; branch: string; filePath: string; }; const pipeline = promisify(Stream.pipeline); async function downloadTar(url: string) { const tempFile = join(tmpdir(), `next.js-cna-example.temp-${Date.now()}`); await pipeline(got.stream(url), createWriteStream(tempFile)); return tempFile; } export async function downloadAndExtractRepo( root: string, { username, name, branch, filePath }: RepoInfo, ) { await makeDir(root); const tempFile = await downloadTar( `https://codeload.github.com/${username}/${name}/tar.gz/${branch}`, ); await tar.x({ file: tempFile, cwd: root, strip: filePath ? filePath.split("/").length + 1 : 1, filter: (p) => p.startsWith( `${name}-${branch.replace(/\//g, "-")}${ filePath ? `/${filePath}/` : "/" }`, ), }); await promises.unlink(tempFile); } const getRepoInfo = async (owner: string, repo: string) => { const repoInfoRes = await got( `https://api.github.com/repos/${owner}/${repo}`, { responseType: "json", }, ); const data = repoInfoRes.body as any; return data; }; export async function getProjectOptions( owner: string, repo: string, ): Promise< { value: CommunityProjectConfig; title: string; }[] > { // TODO: consider using octokit (https://github.com/octokit) if more changes are needed in the future const getCommunityProjectConfig = async ( item: any, ): Promise<CommunityProjectConfig | null> => { // if item is a folder, return the path with default owner, repo, and main branch if (item.type === "dir") return { owner, repo, branch: "main", filePath: item.path, }; // check if it's a submodule (has size = 0 and different owner & repo) if (item.type === "file") { if (item.size !== 0) return null; // submodules have size = 0 // get owner and repo from git_url const { git_url } = item; const startIndex = git_url.indexOf("repos/") + 6; const endIndex = git_url.indexOf("/git"); const ownerRepoStr = git_url.substring(startIndex, endIndex); const [owner, repo] = ownerRepoStr.split("/"); // quick fetch repo info to get the default branch const { default_branch } = await getRepoInfo(owner, repo); // return the path with default owner, repo, and main branch (path is empty for submodules) return { owner, repo, branch: default_branch, }; } return null; }; const url = `https://api.github.com/repos/${owner}/${repo}/contents`; const response = await got(url, { responseType: "json", }); const data = response.body as any[]; const projectConfigs: CommunityProjectConfig[] = []; for (const item of data) { const communityProjectConfig = await getCommunityProjectConfig(item); if (communityProjectConfig) projectConfigs.push(communityProjectConfig); } return projectConfigs.map((config) => { return { value: config, title: config.filePath || config.repo, // for submodules, use repo name as title }; }); } export async function getRepoRawContent(repoFilePath: string) { const url = `https://raw.githubusercontent.com/${repoFilePath}`; const response = await got(url, { responseType: "text", }); return response.body; }