diff --git a/create-turbo/__tests__/__data__/a/b b/create-turbo/__tests__/__data__/a/b new file mode 100755 index 0000000000000..e7df6ee3390c2 --- /dev/null +++ b/create-turbo/__tests__/__data__/a/b @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "test" \ No newline at end of file diff --git a/create-turbo/__tests__/__data__/c/.gitrecognize b/create-turbo/__tests__/__data__/c/.gitrecognize new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/create-turbo/__tests__/__data__/d/e.command b/create-turbo/__tests__/__data__/d/e.command new file mode 100755 index 0000000000000..e7df6ee3390c2 --- /dev/null +++ b/create-turbo/__tests__/__data__/d/e.command @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "test" \ No newline at end of file diff --git a/create-turbo/__tests__/cli.test.ts b/create-turbo/__tests__/cli.test.ts index 3c95a39f3e665..9b7ba84c1f82a 100644 --- a/create-turbo/__tests__/cli.test.ts +++ b/create-turbo/__tests__/cli.test.ts @@ -67,16 +67,21 @@ describe("create-turbo cli", () => { break; case 3: - // Which package manager do you want to use? - // easy to change deployment targets. - expect(getPromptChoices(prompt)).toEqual(["Yarn", "NPM"]); + const chocies = getPromptChoices(prompt); + + // yarn is optional. however, npm should be present on almost + // every system. + expect(chocies).toContain("NPM"); + + // just hit enter to pick the first result from the list. cli.stdin.write(keys.enter); break; case 4: - expect(prompt).toEqual( - "? Do you want me to run `yarn install`? (Y/n)" + expect(prompt).toMatch( + /\? Do you want me to run \`(yarn|npm) install\`\? \(Y\/n\)/ ); + cli.stdin.write("n"); // At this point the CLI will create directories and all that fun stuff diff --git a/create-turbo/__tests__/hasExecutable.test.ts b/create-turbo/__tests__/hasExecutable.test.ts new file mode 100644 index 0000000000000..e24af7e59cfce --- /dev/null +++ b/create-turbo/__tests__/hasExecutable.test.ts @@ -0,0 +1,29 @@ +import path from "path"; + +import { hasExecutable } from "../src/hasExecutable"; + +const baseDirectory = path.join(__dirname, "__data__"); + +describe("hasExecutable", () => { + test("works with process.env.PATH and process.env.PATHEXT", async () => { + expect(await hasExecutable("node")).toBeTruthy(); + }); + + test("returns true, if the executable is present", async () => { + expect( + await hasExecutable("b", [path.resolve(baseDirectory, "a")]) + ).toBeTruthy(); + }); + + test("returns false, if the executable is not present", async () => { + expect( + await hasExecutable("c", [path.resolve(baseDirectory, "c")]) + ).toBeFalsy(); + }); + + test("works with extensions", async () => { + expect( + await hasExecutable("e", [path.resolve(baseDirectory, "d")], [".command"]) + ).toBeTruthy(); + }); +}); diff --git a/create-turbo/src/hasExecutable.ts b/create-turbo/src/hasExecutable.ts new file mode 100644 index 0000000000000..5239d254fae00 --- /dev/null +++ b/create-turbo/src/hasExecutable.ts @@ -0,0 +1,42 @@ +import path from "path"; +import fs from "fs/promises"; + +const environmentPaths = (process.env.PATH || "") + .replace(/["]+/g, "") + .split(path.delimiter) + .filter(Boolean); + +const environmentExtensions = (process.env.PATHEXT || "").split(";"); + +/** + * Determines whether or not the given executable is present on the system. + * + * Inspired by https://github.com/springernature/hasbin/blob/master/lib/hasbin.js#L55 + * + * @param name + */ +export async function hasExecutable( + executable: string, + paths = environmentPaths, + extensions = environmentExtensions +) { + try { + return await Promise.any( + paths + .flatMap((d) => extensions.map((ext) => path.join(d, executable + ext))) + .map(isFilePresent) + ); + } catch (err) { + return false; + } +} + +async function isFilePresent(path: string) { + const stats = await fs.stat(path); + + if (stats.isFile()) { + return true; + } + + throw new Error(`${path} is not a file`); +} diff --git a/create-turbo/src/index.ts b/create-turbo/src/index.ts index 1ecf1c4f7004a..cc41e3b1b3c67 100644 --- a/create-turbo/src/index.ts +++ b/create-turbo/src/index.ts @@ -12,6 +12,7 @@ import chalk from "chalk"; import cliPkgJson from "../package.json"; import { shouldUseYarn } from "./shouldUseYarn"; import { tryGitInit } from "./git"; +import { hasExecutable } from "./hasExecutable"; const turboGradient = gradient("#0099F7", "#F11712"); const help = ` @@ -88,7 +89,11 @@ async function run() { type: "list", message: "Which package manager do you want to use?", choices: [ - { name: "Yarn", value: "yarn" }, + { + name: "Yarn", + value: "yarn", + disabled: !(await hasExecutable("yarn")), + }, { name: "NPM", value: "npm" }, // { name: "PNPM", value: "pnpm" }, ], @@ -246,7 +251,7 @@ async function notifyUpdate(): Promise { try { const res = await update; if (res?.latest) { - const isYarn = shouldUseYarn(); + const isYarn = await shouldUseYarn(); console.log(); console.log( diff --git a/create-turbo/src/shouldUseYarn.ts b/create-turbo/src/shouldUseYarn.ts index 0153ae01e5548..8e40393dc3983 100644 --- a/create-turbo/src/shouldUseYarn.ts +++ b/create-turbo/src/shouldUseYarn.ts @@ -1,14 +1,10 @@ -import { execSync } from "child_process"; +import { hasExecutable } from "./hasExecutable"; -export function shouldUseYarn(): boolean { - try { - const userAgent = process.env.npm_config_user_agent; - if (userAgent) { - return Boolean(userAgent && userAgent.startsWith("yarn")); - } - execSync("yarnpkg --version", { stdio: "ignore" }); - return true; - } catch (e) { - return false; +export async function shouldUseYarn() { + const userAgent = process.env.npm_config_user_agent; + if (userAgent) { + return Boolean(userAgent && userAgent.startsWith("yarn")); } + + return hasExecutable("yarn"); }