diff --git a/packages/create-turbo/__tests__/index.test.ts b/packages/create-turbo/__tests__/index.test.ts index 2984f6b9861d4..cb5313dba6f77 100644 --- a/packages/create-turbo/__tests__/index.test.ts +++ b/packages/create-turbo/__tests__/index.test.ts @@ -1,15 +1,14 @@ -import path from "path"; +import path from "node:path"; +import childProcess from "node:child_process"; import chalk from "chalk"; -import childProcess from "child_process"; -import { setupTestFixtures, spyConsole } from "@turbo/test-utils"; -import { create } from "../src/commands/create"; -import type { CreateCommandArgument } from "../src/commands/create/types"; +import { setupTestFixtures, spyConsole, spyExit } from "@turbo/test-utils"; import { logger } from "@turbo/utils"; import type { PackageManager } from "@turbo/workspaces"; - // imports for mocks import * as turboWorkspaces from "@turbo/workspaces"; import * as turboUtils from "@turbo/utils"; +import type { CreateCommandArgument } from "../src/commands/create/types"; +import { create } from "../src/commands/create"; import { getWorkspaceDetailsMockReturnValue } from "./test-utils"; jest.mock("@turbo/workspaces", () => ({ @@ -24,6 +23,7 @@ describe("create-turbo", () => { }); const mockConsole = spyConsole(); + const mockExit = spyExit(); test.each<{ packageManager: PackageManager }>([ { packageManager: "yarn" }, @@ -97,4 +97,62 @@ describe("create-turbo", () => { mockExecSync.mockRestore(); } ); + + test.only("throws correct error message when a download error is encountered", async () => { + const { root } = useFixture({ fixture: `create-turbo` }); + const packageManager = "pnpm"; + const mockAvailablePackageManagers = jest + .spyOn(turboUtils, "getAvailablePackageManagers") + .mockResolvedValue({ + npm: "8.19.2", + yarn: "1.22.10", + pnpm: "7.22.2", + }); + + const mockCreateProject = jest + .spyOn(turboUtils, "createProject") + .mockRejectedValue(new turboUtils.DownloadError("Could not connect")); + + const mockGetWorkspaceDetails = jest + .spyOn(turboWorkspaces, "getWorkspaceDetails") + .mockResolvedValue( + getWorkspaceDetailsMockReturnValue({ + root, + packageManager, + }) + ); + + const mockExecSync = jest + .spyOn(childProcess, "execSync") + .mockImplementation(() => { + return "success"; + }); + + await create( + root as CreateCommandArgument, + packageManager as CreateCommandArgument, + { + skipInstall: true, + example: "default", + } + ); + + expect(mockConsole.error).toHaveBeenCalledTimes(2); + expect(mockConsole.error).toHaveBeenNthCalledWith( + 1, + logger.turboRed.bold(">>>"), + chalk.red("Unable to download template from Github") + ); + expect(mockConsole.error).toHaveBeenNthCalledWith( + 2, + logger.turboRed.bold(">>>"), + chalk.red("Could not connect") + ); + expect(mockExit.exit).toHaveBeenCalledWith(1); + + mockAvailablePackageManagers.mockRestore(); + mockCreateProject.mockRestore(); + mockGetWorkspaceDetails.mockRestore(); + mockExecSync.mockRestore(); + }); }); diff --git a/packages/create-turbo/package.json b/packages/create-turbo/package.json index 76db941e2460e..5fa5aad010681 100644 --- a/packages/create-turbo/package.json +++ b/packages/create-turbo/package.json @@ -47,7 +47,7 @@ "jest": "^27.4.3", "ts-jest": "^27.1.1", "tsup": "^6.7.0", - "typescript": "^4.5.5" + "typescript": "^5.2.2" }, "files": [ "dist" diff --git a/packages/create-turbo/src/commands/create/index.ts b/packages/create-turbo/src/commands/create/index.ts index 4355b4cca52b9..2daf46dcf9d25 100644 --- a/packages/create-turbo/src/commands/create/index.ts +++ b/packages/create-turbo/src/commands/create/index.ts @@ -10,6 +10,7 @@ import { import { getAvailablePackageManagers, createProject, + DownloadError, logger, } from "@turbo/utils"; import { tryGitCommit, tryGitInit } from "../../utils/git"; @@ -33,8 +34,15 @@ function handleErrors(err: unknown) { } else if (err instanceof ConvertError && err.type !== "unknown") { error(chalk.red(err.message)); process.exit(1); - // handle unknown errors (no special handling, just re-throw to catch at root) - } else { + // handle download errors from @turbo/utils + } else if (err instanceof DownloadError) { + error(chalk.red("Unable to download template from Github")); + error(chalk.red(err.message)); + process.exit(1); + } + + // handle unknown errors (no special handling, just re-throw to catch at root) + else { throw err; } } @@ -85,12 +93,20 @@ export async function create( const { example, examplePath } = opts; const exampleName = example && example !== "default" ? example : "basic"; - const { hasPackageJson, availableScripts, repoInfo } = await createProject({ - appPath: root, - example: exampleName, - isDefaultExample: isDefaultExample(exampleName), - examplePath, - }); + + let projectData = {} as Awaited>; + try { + projectData = await createProject({ + appPath: root, + example: exampleName, + isDefaultExample: isDefaultExample(exampleName), + examplePath, + }); + } catch (err) { + handleErrors(err); + } + + const { hasPackageJson, availableScripts, repoInfo } = projectData; // create a new git repo after creating the project tryGitInit(root, `feat(create-turbo): create ${exampleName}`); diff --git a/packages/turbo-utils/src/index.ts b/packages/turbo-utils/src/index.ts index 0e21759604660..b6f5d3970530d 100644 --- a/packages/turbo-utils/src/index.ts +++ b/packages/turbo-utils/src/index.ts @@ -17,7 +17,7 @@ export { downloadAndExtractExample, } from "./examples"; export { isWriteable } from "./isWriteable"; -export { createProject } from "./createProject"; +export { createProject, DownloadError } from "./createProject"; export { convertCase } from "./convertCase"; export * as logger from "./logger"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c4e47ff1c7e4..9f7a25cbe037f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -500,13 +500,13 @@ importers: version: 27.5.1(ts-node@10.9.1) ts-jest: specifier: ^27.1.1 - version: 27.1.5(@babel/core@7.20.12)(@types/jest@27.5.2)(esbuild@0.17.18)(jest@27.5.1)(typescript@4.9.4) + version: 27.1.5(@babel/core@7.20.12)(@types/jest@27.5.2)(esbuild@0.17.18)(jest@27.5.1)(typescript@5.2.2) tsup: specifier: ^6.7.0 - version: 6.7.0(ts-node@10.9.1)(typescript@4.9.4) + version: 6.7.0(typescript@5.2.2) typescript: - specifier: ^4.5.5 - version: 4.9.4 + specifier: ^5.2.2 + version: 5.2.2 packages/devlow-bench: dependencies: @@ -894,7 +894,7 @@ importers: version: 27.5.1(ts-node@10.9.1) ts-jest: specifier: ^27.1.1 - version: 27.1.5(@babel/core@7.20.12)(@types/jest@27.5.2)(esbuild@0.17.18)(jest@27.5.1)(typescript@4.9.4) + version: 27.1.5(@babel/core@7.20.12)(@types/jest@27.5.2)(esbuild@0.14.49)(jest@27.5.1)(typescript@4.9.4) typescript: specifier: ^4.7.4 version: 4.9.4 @@ -13451,6 +13451,42 @@ packages: yargs-parser: 20.2.9 dev: true + /ts-jest@27.1.5(@babel/core@7.20.12)(@types/jest@27.5.2)(esbuild@0.17.18)(jest@27.5.1)(typescript@5.2.2): + resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@types/jest': ^27.0.0 + babel-jest: '>=27.0.0 <28' + esbuild: '*' + jest: ^27.0.0 + typescript: '>=3.8 <5.0' + peerDependenciesMeta: + '@babel/core': + optional: true + '@types/jest': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.20.12 + '@types/jest': 27.5.2 + bs-logger: 0.2.6 + esbuild: 0.17.18 + fast-json-stable-stringify: 2.1.0 + jest: 27.5.1(ts-node@10.9.1) + jest-util: 27.5.1 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.0 + typescript: 5.2.2 + yargs-parser: 20.2.9 + dev: true + /ts-json-schema-generator@1.1.2: resolution: {integrity: sha512-XMnxvndJFJEYv3NBmW7Po5bGajKdK2qH8Q078eDy60srK9+nEvbT9nLCRKd2IV/RQ7a+oc5FNylvZWveqh7jeQ==} engines: {node: '>=10.0.0'} @@ -13588,6 +13624,42 @@ packages: - ts-node dev: true + /tsup@6.7.0(typescript@5.2.2): + resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} + engines: {node: '>=14.18'} + hasBin: true + peerDependencies: + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.1.0' + peerDependenciesMeta: + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.0.1(esbuild@0.17.18) + cac: 6.7.12 + chokidar: 3.5.3 + debug: 4.3.4 + esbuild: 0.17.18 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 3.1.4(ts-node@10.9.1) + resolve-from: 5.0.0 + rollup: 3.21.5 + source-map: 0.8.0-beta.0 + sucrase: 3.24.0 + tree-kill: 1.2.2 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + /tsutils@3.21.0(typescript@4.9.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -13707,6 +13779,12 @@ packages: engines: {node: '>=4.2.0'} hasBin: true + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + /uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'}