UNPKG

create-t3-app-deepmeta

Version:
334 lines (297 loc) 10.9 kB
import { type AvailablePackages } from "~/installers/index.js"; import { availablePackages } from "~/installers/index.js"; import chalk from "chalk"; import { Command } from "commander"; import inquirer from "inquirer"; import { CREATE_T3_APP, DEFAULT_APP_NAME } from "~/consts.js"; import { getVersion } from "~/utils/getT3Version.js"; import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; import { logger } from "~/utils/logger.js"; import { validateAppName } from "~/utils/validateAppName.js"; import { validateImportAlias } from "~/utils/validateImportAlias.js"; interface CliFlags { noGit: boolean; noInstall: boolean; default: boolean; importAlias: string; /** @internal Used in CI. */ CI: boolean; /** @internal Used in CI. */ tailwind: boolean; /** @internal Used in CI. */ trpc: boolean; /** @internal Used in CI. */ prisma: boolean; /** @internal Used in CI. */ nextAuth: boolean; } interface CliResults { appName: string; packages: AvailablePackages[]; flags: CliFlags; } const defaultOptions: CliResults = { appName: DEFAULT_APP_NAME, packages: ["nextAuth", "prisma", "tailwind", "trpc"], flags: { noGit: false, noInstall: false, default: false, CI: false, tailwind: false, trpc: false, prisma: false, nextAuth: false, importAlias: "~/", }, }; export const runCli = async () => { const cliResults = defaultOptions; const program = new Command().name(CREATE_T3_APP); // TODO: This doesn't return anything typesafe. Research other options? // Emulate from: https://github.com/Schniz/soundtype-commander program .description("A CLI for creating web applications with the t3 stack") .argument( "[dir]", "The name of the application, as well as the name of the directory to create", ) .option( "--noGit", "Explicitly tell the CLI to not initialize a new git repo in the project", false, ) .option( "--noInstall", "Explicitly tell the CLI to not run the package manager's install command", false, ) .option( "-y, --default", "Bypass the CLI and use all default options to bootstrap a new t3-app", false, ) /** START CI-FLAGS */ /** * @experimental Used for CI E2E tests. If any of the following option-flags are provided, we * skip prompting. */ .option("--CI", "Boolean value if we're running in CI", false) /** @experimental - Used for CI E2E tests. Used in conjunction with `--CI` to skip prompting. */ .option( "--tailwind [boolean]", "Experimental: Boolean value if we should install Tailwind CSS. Must be used in conjunction with `--CI`.", (value) => !!value && value !== "false", ) /** @experimental Used for CI E2E tests. Used in conjunction with `--CI` to skip prompting. */ .option( "--nextAuth [boolean]", "Experimental: Boolean value if we should install NextAuth.js. Must be used in conjunction with `--CI`.", (value) => !!value && value !== "false", ) /** @experimental - Used for CI E2E tests. Used in conjunction with `--CI` to skip prompting. */ .option( "--prisma [boolean]", "Experimental: Boolean value if we should install Prisma. Must be used in conjunction with `--CI`.", (value) => !!value && value !== "false", ) /** @experimental - Used for CI E2E tests. Used in conjunction with `--CI` to skip prompting. */ .option( "--trpc [boolean]", "Experimental: Boolean value if we should install tRPC. Must be used in conjunction with `--CI`.", (value) => !!value && value !== "false", ) /** @experimental - Used for CI E2E tests. Used in conjunction with `--CI` to skip prompting. */ .option( "-i, --import-alias", "Explicitly tell the CLI to use a custom import alias", defaultOptions.flags.importAlias, ) /** END CI-FLAGS */ .version(getVersion(), "-v, --version", "Display the version number") .addHelpText( "afterAll", `\n The t3 stack was inspired by ${chalk .hex("#E8DCFF") .bold( "@t3dotgg", )} and has been used to build awesome fullstack applications like ${chalk .hex("#E24A8D") .underline("https://ping.gg")} \n`, ) .parse(process.argv); // FIXME: TEMPORARY WARNING WHEN USING YARN 3. SEE ISSUE #57 if (process.env.npm_config_user_agent?.startsWith("yarn/3")) { logger.warn(` WARNING: It looks like you are using Yarn 3. This is currently not supported, and likely to result in a crash. Please run create-t3-app with another package manager such as pnpm, npm, or Yarn Classic. See: https://github.com/t3-oss/create-t3-app/issues/57`); } // Needs to be separated outside the if statement to correctly infer the type as string | undefined const cliProvidedName = program.args[0]; if (cliProvidedName) { cliResults.appName = cliProvidedName; } cliResults.flags = program.opts(); /** @internal Used for CI E2E tests. */ let CIMode = false; if (cliResults.flags.CI) { CIMode = true; cliResults.packages = []; if (cliResults.flags.trpc) cliResults.packages.push("trpc"); if (cliResults.flags.tailwind) cliResults.packages.push("tailwind"); if (cliResults.flags.prisma) cliResults.packages.push("prisma"); if (cliResults.flags.nextAuth) cliResults.packages.push("nextAuth"); } // Explained below why this is in a try/catch block try { if ( process.env.SHELL?.toLowerCase().includes("git") && process.env.SHELL?.includes("bash") ) { logger.warn(` WARNING: It looks like you are using Git Bash which is non-interactive. Please run create-t3-app with another terminal such as Windows Terminal or PowerShell if you want to use the interactive CLI.`); const error = new Error("Non-interactive environment"); // eslint-disable-next-line @typescript-eslint/no-explicit-any (error as any).isTTYError = true; throw error; } // if --CI flag is set, we are running in CI mode and should not prompt the user // if --default flag is set, we should not prompt the user if (!cliResults.flags.default && !CIMode) { if (!cliProvidedName) { cliResults.appName = await promptAppName(); } await promptLanguage(); cliResults.packages = await promptPackages(); if (!cliResults.flags.noGit) { cliResults.flags.noGit = !(await promptGit()); } if (!cliResults.flags.noInstall) { cliResults.flags.noInstall = !(await promptInstall()); } cliResults.flags.importAlias = await promptImportAlias(); } } catch (err) { // If the user is not calling create-t3-app from an interactive terminal, inquirer will throw an error with isTTYError = true // If this happens, we catch the error, tell the user what has happened, and then continue to run the program with a default t3 app // Otherwise we have to do some fancy namespace extension logic on the Error type which feels overkill for one line // eslint-disable-next-line @typescript-eslint/no-explicit-any if (err instanceof Error && (err as any).isTTYError) { logger.warn(` ${CREATE_T3_APP} needs an interactive terminal to provide options`); const { shouldContinue } = await inquirer.prompt<{ shouldContinue: boolean; }>({ name: "shouldContinue", type: "confirm", message: `Continue scaffolding a default T3 app?`, default: true, }); if (!shouldContinue) { logger.info("Exiting..."); process.exit(0); } logger.info(`Bootstrapping a default T3 app in ./${cliResults.appName}`); } else { throw err; } } return cliResults; }; const promptAppName = async (): Promise<string> => { const { appName } = await inquirer.prompt<Pick<CliResults, "appName">>({ name: "appName", type: "input", message: "What will your project be called?", default: defaultOptions.appName, validate: validateAppName, transformer: (input: string) => { return input.trim(); }, }); return appName; }; const promptLanguage = async (): Promise<void> => { const { language } = await inquirer.prompt<{ language: string }>({ name: "language", type: "list", message: "Will you be using TypeScript or JavaScript?", choices: [ { name: "TypeScript", value: "typescript", short: "TypeScript" }, { name: "JavaScript", value: "javascript", short: "JavaScript" }, ], default: "typescript", }); if (language === "javascript") { logger.error("Wrong answer, using TypeScript instead..."); } else { logger.success("Good choice! Using TypeScript!"); } }; const promptPackages = async (): Promise<AvailablePackages[]> => { const { packages } = await inquirer.prompt<Pick<CliResults, "packages">>({ name: "packages", type: "checkbox", message: "Which packages would you like to enable?", choices: availablePackages .filter((pkg) => pkg !== "envVariables") // don't prompt for env-vars .map((pkgName) => ({ name: pkgName, checked: false, })), }); return packages; }; const promptGit = async (): Promise<boolean> => { const { git } = await inquirer.prompt<{ git: boolean }>({ name: "git", type: "confirm", message: "Initialize a new git repository?", default: true, }); if (git) { logger.success("Nice one! Initializing repository!"); } else { logger.info("Sounds good! You can come back and run git init later."); } return git; }; const promptInstall = async (): Promise<boolean> => { const pkgManager = getUserPkgManager(); const { install } = await inquirer.prompt<{ install: boolean }>({ name: "install", type: "confirm", message: `Would you like us to run '${pkgManager}` + (pkgManager === "yarn" ? `'?` : ` install'?`), default: true, }); if (install) { logger.success("Alright. We'll install the dependencies for you!"); } else { if (pkgManager === "yarn") { logger.info( `No worries. You can run '${pkgManager}' later to install the dependencies.`, ); } else { logger.info( `No worries. You can run '${pkgManager} install' later to install the dependencies.`, ); } } return install; }; const promptImportAlias = async (): Promise<string> => { const { importAlias } = await inquirer.prompt<Pick<CliFlags, "importAlias">>({ name: "importAlias", type: "input", message: "What import alias would you like configured?", default: defaultOptions.flags.importAlias, validate: validateImportAlias, transformer: (input: string) => { return input.trim(); }, }); return importAlias; };