From dd84b9d64fb98746a230cd24233ff50a562c39c9 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Fri, 28 Apr 2023 01:36:44 +0800 Subject: --- packages/create-turbo/src/utils/examples.ts | 139 +++++++++++++++++++++ packages/create-turbo/src/utils/git.ts | 90 +++++++++++++ .../create-turbo/src/utils/isDefaultExample.ts | 5 + packages/create-turbo/src/utils/isFolderEmpty.ts | 37 ++++++ packages/create-turbo/src/utils/isOnline.ts | 40 ++++++ packages/create-turbo/src/utils/isWriteable.ts | 10 ++ packages/create-turbo/src/utils/notifyUpdate.ts | 22 ++++ 7 files changed, 343 insertions(+) create mode 100644 packages/create-turbo/src/utils/examples.ts create mode 100644 packages/create-turbo/src/utils/git.ts create mode 100644 packages/create-turbo/src/utils/isDefaultExample.ts create mode 100644 packages/create-turbo/src/utils/isFolderEmpty.ts create mode 100644 packages/create-turbo/src/utils/isOnline.ts create mode 100644 packages/create-turbo/src/utils/isWriteable.ts create mode 100644 packages/create-turbo/src/utils/notifyUpdate.ts (limited to 'packages/create-turbo/src/utils') 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 { + try { + const res = await got.head(url); + return res.statusCode === 200; + } catch (err) { + return false; + } +} + +export async function getRepoInfo( + url: URL, + examplePath?: string +): Promise { + 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 { + 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 { + 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; +} { + 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 { + 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 { + 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 { + 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 + } +} -- cgit v1.2.3-70-g09d2