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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
import got from "got";
import tar from "tar";
import { Stream } from "stream";
import { promisify } from "util";
import { join } from "path";
import { tmpdir } from "os";
import { createWriteStream, promises as fs } from "fs";
const pipeline = promisify(Stream.pipeline);
export type RepoInfo = {
username: string;
name: string;
branch: string;
filePath: string;
};
export async function isUrlOk(url: string): Promise<boolean> {
try {
const res = await got.head(url);
return res.statusCode === 200;
} catch (err) {
return false;
}
}
export async function getRepoInfo(
url: URL,
examplePath?: string
): Promise<RepoInfo | undefined> {
const [, username, name, tree, sourceBranch, ...file] =
url.pathname.split("/");
const filePath = examplePath
? examplePath.replace(/^\//, "")
: file.join("/");
if (
// Support repos whose entire purpose is to be a Turborepo example, e.g.
// https://github.com/:username/:my-cool-turborepo-example-repo-name.
tree === undefined ||
// Support GitHub URL that ends with a trailing slash, e.g.
// https://github.com/:username/:my-cool-turborepo-example-repo-name/
// In this case "t" will be an empty string while the turbo part "_branch" will be undefined
(tree === "" && sourceBranch === undefined)
) {
try {
const infoResponse = await got(
`https://api.github.com/repos/${username}/${name}`
);
const info = JSON.parse(infoResponse.body);
return { username, name, branch: info["default_branch"], filePath };
} catch (err) {
return;
}
}
// If examplePath is available, the branch name takes the entire path
const branch = examplePath
? `${sourceBranch}/${file.join("/")}`.replace(
new RegExp(`/${filePath}|/$`),
""
)
: sourceBranch;
if (username && name && branch && tree === "tree") {
return { username, name, branch, filePath };
}
}
export function hasRepo({
username,
name,
branch,
filePath,
}: RepoInfo): Promise<boolean> {
const contentsUrl = `https://api.github.com/repos/${username}/${name}/contents`;
const packagePath = `${filePath ? `/${filePath}` : ""}/package.json`;
return isUrlOk(contentsUrl + packagePath + `?ref=${branch}`);
}
export function existsInRepo(nameOrUrl: string): Promise<boolean> {
try {
const url = new URL(nameOrUrl);
return isUrlOk(url.href);
} catch {
return isUrlOk(
`https://api.github.com/repos/vercel/turbo/contents/examples/${encodeURIComponent(
nameOrUrl
)}`
);
}
}
async function downloadTar(url: string, name: string) {
const tempFile = join(tmpdir(), `${name}.temp-${Date.now()}`);
await pipeline(got.stream(url), createWriteStream(tempFile));
return tempFile;
}
export async function downloadAndExtractRepo(
root: string,
{ username, name, branch, filePath }: RepoInfo
) {
const tempFile = await downloadTar(
`https://codeload.github.com/${username}/${name}/tar.gz/${branch}`,
`turbo-ct-example`
);
await tar.x({
file: tempFile,
cwd: root,
strip: filePath ? filePath.split("/").length + 1 : 1,
filter: (p: string) =>
p.startsWith(
`${name}-${branch.replace(/\//g, "-")}${
filePath ? `/${filePath}/` : "/"
}`
),
});
await fs.unlink(tempFile);
}
export async function downloadAndExtractExample(root: string, name: string) {
const tempFile = await downloadTar(
`https://codeload.github.com/vercel/turbo/tar.gz/main`,
`turbo-ct-example`
);
await tar.x({
file: tempFile,
cwd: root,
strip: 2 + name.split("/").length,
filter: (p: string) => p.includes(`turbo-main/examples/${name}/`),
});
await fs.unlink(tempFile);
}
|