aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/create-turbo/src/utils
diff options
context:
space:
mode:
Diffstat (limited to 'packages/create-turbo/src/utils')
-rw-r--r--packages/create-turbo/src/utils/examples.ts139
-rw-r--r--packages/create-turbo/src/utils/git.ts90
-rw-r--r--packages/create-turbo/src/utils/isDefaultExample.ts5
-rw-r--r--packages/create-turbo/src/utils/isFolderEmpty.ts37
-rw-r--r--packages/create-turbo/src/utils/isOnline.ts40
-rw-r--r--packages/create-turbo/src/utils/isWriteable.ts10
-rw-r--r--packages/create-turbo/src/utils/notifyUpdate.ts22
7 files changed, 343 insertions, 0 deletions
diff --git a/packages/create-turbo/src/utils/examples.ts b/packages/create-turbo/src/utils/examples.ts
new file mode 100644
index 0000000..b7c4812
--- /dev/null
+++ b/packages/create-turbo/src/utils/examples.ts
@@ -0,0 +1,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);
+}
diff --git a/packages/create-turbo/src/utils/git.ts b/packages/create-turbo/src/utils/git.ts
new file mode 100644
index 0000000..593e7ea
--- /dev/null
+++ b/packages/create-turbo/src/utils/git.ts
@@ -0,0 +1,90 @@
+import fs from "fs-extra";
+import { execSync } from "child_process";
+import path from "path";
+import rimraf from "rimraf";
+
+export const DEFAULT_IGNORE = `
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+node_modules
+.pnp
+.pnp.js
+
+# testing
+coverage
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# turbo
+.turbo
+
+# vercel
+.vercel
+`;
+
+export const GIT_REPO_COMMAND = "git rev-parse --is-inside-work-tree";
+export const HG_REPO_COMMAND = "hg --cwd . root";
+
+export function isInGitRepository(): boolean {
+ try {
+ execSync(GIT_REPO_COMMAND, { stdio: "ignore" });
+ return true;
+ } catch (_) {}
+ return false;
+}
+
+export function isInMercurialRepository(): boolean {
+ try {
+ execSync(HG_REPO_COMMAND, { stdio: "ignore" });
+ return true;
+ } catch (_) {}
+ return false;
+}
+
+export function tryGitInit(root: string, message: string): boolean {
+ let didInit = false;
+ try {
+ execSync("git --version", { stdio: "ignore" });
+ if (isInGitRepository() || isInMercurialRepository()) {
+ return false;
+ }
+
+ execSync("git init", { stdio: "ignore" });
+ didInit = true;
+
+ execSync("git checkout -b main", { stdio: "ignore" });
+
+ execSync("git add -A", { stdio: "ignore" });
+ execSync(`git commit -m "${message}"`, {
+ stdio: "ignore",
+ });
+ return true;
+ } catch (err) {
+ if (didInit) {
+ try {
+ rimraf.sync(path.join(root, ".git"));
+ } catch (_) {}
+ }
+ return false;
+ }
+}
+
+export function tryGitCommit(message: string): boolean {
+ try {
+ execSync("git add -A", { stdio: "ignore" });
+ execSync(`git commit -m "${message}"`, {
+ stdio: "ignore",
+ });
+ return true;
+ } catch (err) {
+ return false;
+ }
+}
diff --git a/packages/create-turbo/src/utils/isDefaultExample.ts b/packages/create-turbo/src/utils/isDefaultExample.ts
new file mode 100644
index 0000000..9fb2ef2
--- /dev/null
+++ b/packages/create-turbo/src/utils/isDefaultExample.ts
@@ -0,0 +1,5 @@
+export const DEFAULT_EXAMPLES = new Set(["basic", "default"]);
+
+export function isDefaultExample(example: string): boolean {
+ return DEFAULT_EXAMPLES.has(example);
+}
diff --git a/packages/create-turbo/src/utils/isFolderEmpty.ts b/packages/create-turbo/src/utils/isFolderEmpty.ts
new file mode 100644
index 0000000..4de2d58
--- /dev/null
+++ b/packages/create-turbo/src/utils/isFolderEmpty.ts
@@ -0,0 +1,37 @@
+import fs from "fs-extra";
+
+const VALID_FILES = [
+ ".DS_Store",
+ ".git",
+ ".gitattributes",
+ ".gitignore",
+ ".gitlab-ci.yml",
+ ".hg",
+ ".hgcheck",
+ ".hgignore",
+ ".idea",
+ ".npmignore",
+ ".travis.yml",
+ "LICENSE",
+ "Thumbs.db",
+ "docs",
+ "mkdocs.yml",
+ "npm-debug.log",
+ "yarn-debug.log",
+ "yarn-error.log",
+ "yarnrc.yml",
+ ".yarn",
+];
+
+export function isFolderEmpty(root: string): {
+ isEmpty: boolean;
+ conflicts: Array<string>;
+} {
+ const conflicts = fs
+ .readdirSync(root)
+ .filter((file) => !VALID_FILES.includes(file))
+ // Support IntelliJ IDEA-based editors
+ .filter((file) => !/\.iml$/.test(file));
+
+ return { isEmpty: conflicts.length === 0, conflicts };
+}
diff --git a/packages/create-turbo/src/utils/isOnline.ts b/packages/create-turbo/src/utils/isOnline.ts
new file mode 100644
index 0000000..f02b2e6
--- /dev/null
+++ b/packages/create-turbo/src/utils/isOnline.ts
@@ -0,0 +1,40 @@
+import { execSync } from "child_process";
+import dns from "dns";
+import url from "url";
+
+function getProxy(): string | undefined {
+ if (process.env.https_proxy) {
+ return process.env.https_proxy;
+ }
+
+ try {
+ const httpsProxy = execSync("npm config get https-proxy").toString().trim();
+ return httpsProxy !== "null" ? httpsProxy : undefined;
+ } catch (e) {
+ return;
+ }
+}
+
+export function isOnline(): Promise<boolean> {
+ return new Promise((resolve) => {
+ dns.lookup("registry.yarnpkg.com", (registryErr) => {
+ if (!registryErr) {
+ return resolve(true);
+ }
+
+ const proxy = getProxy();
+ if (!proxy) {
+ return resolve(false);
+ }
+
+ const { hostname } = url.parse(proxy);
+ if (!hostname) {
+ return resolve(false);
+ }
+
+ dns.lookup(hostname, (proxyErr) => {
+ resolve(proxyErr == null);
+ });
+ });
+ });
+}
diff --git a/packages/create-turbo/src/utils/isWriteable.ts b/packages/create-turbo/src/utils/isWriteable.ts
new file mode 100644
index 0000000..132c42a
--- /dev/null
+++ b/packages/create-turbo/src/utils/isWriteable.ts
@@ -0,0 +1,10 @@
+import fs from "fs-extra";
+
+export async function isWriteable(directory: string): Promise<boolean> {
+ try {
+ await fs.access(directory, (fs.constants || fs).W_OK);
+ return true;
+ } catch (err) {
+ return false;
+ }
+}
diff --git a/packages/create-turbo/src/utils/notifyUpdate.ts b/packages/create-turbo/src/utils/notifyUpdate.ts
new file mode 100644
index 0000000..e1dadc0
--- /dev/null
+++ b/packages/create-turbo/src/utils/notifyUpdate.ts
@@ -0,0 +1,22 @@
+import chalk from "chalk";
+import checkForUpdate from "update-check";
+
+import cliPkgJson from "../../package.json";
+
+const update = checkForUpdate(cliPkgJson).catch(() => null);
+
+export default async function notifyUpdate(): Promise<void> {
+ try {
+ const res = await update;
+ if (res?.latest) {
+ console.log();
+ console.log(
+ chalk.yellow.bold("A new version of `create-turbo` is available!")
+ );
+ console.log();
+ }
+ process.exit();
+ } catch (_e: any) {
+ // ignore error
+ }
+}