diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index 29da93d2aa11b..f2fd2497eed3f 100644 --- a/packages/create-next-app/create-app.ts +++ b/packages/create-next-app/create-app.ts @@ -1,16 +1,16 @@ +import retry from 'async-retry' import chalk from 'chalk' import cpy from 'cpy' import fs from 'fs' import makeDir from 'make-dir' import os from 'os' import path from 'path' - import { - hasExample, - hasRepo, - getRepoInfo, downloadAndExtractExample, downloadAndExtractRepo, + getRepoInfo, + hasExample, + hasRepo, RepoInfo, } from './helpers/examples' import { tryGitInit } from './helpers/git' @@ -19,6 +19,8 @@ import { isFolderEmpty } from './helpers/is-folder-empty' import { getOnline } from './helpers/is-online' import { shouldUseYarn } from './helpers/should-use-yarn' +export class DownloadError extends Error {} + export async function createApp({ appPath, useNpm, @@ -75,7 +77,7 @@ export async function createApp({ ) process.exit(1) } - } else { + } else if (example !== '__internal-testing-retry') { const found = await hasExample(example) if (!found) { @@ -109,24 +111,32 @@ export async function createApp({ process.chdir(root) if (example) { - if (repoInfo) { - console.log( - `Downloading files from repo ${chalk.cyan( - example - )}. This might take a moment.` - ) - console.log() - await downloadAndExtractRepo(root, repoInfo) - } else { - console.log( - `Downloading files for example ${chalk.cyan( - example - )}. This might take a moment.` - ) - console.log() - await downloadAndExtractExample(root, example) + try { + if (repoInfo) { + const repoInfo2 = repoInfo + console.log( + `Downloading files from repo ${chalk.cyan( + example + )}. This might take a moment.` + ) + console.log() + await retry(() => downloadAndExtractRepo(root, repoInfo2), { + retries: 3, + }) + } else { + console.log( + `Downloading files for example ${chalk.cyan( + example + )}. This might take a moment.` + ) + console.log() + await retry(() => downloadAndExtractExample(root, example), { + retries: 3, + }) + } + } catch (reason) { + throw new DownloadError(reason) } - // Copy our default `.gitignore` if the application did not provide one const ignorePath = path.join(root, '.gitignore') if (!fs.existsSync(ignorePath)) { diff --git a/packages/create-next-app/helpers/examples.ts b/packages/create-next-app/helpers/examples.ts index 009105d7baf19..2a75490e4692f 100644 --- a/packages/create-next-app/helpers/examples.ts +++ b/packages/create-next-app/helpers/examples.ts @@ -86,6 +86,10 @@ export async function downloadAndExtractExample( root: string, name: string ): Promise { + if (name === '__internal-testing-retry') { + throw new Error('This is an internal example for testing the CLI.') + } + try { return await pipeline( got.stream('https://codeload.github.com/zeit/next.js/tar.gz/canary'), diff --git a/packages/create-next-app/index.ts b/packages/create-next-app/index.ts index 0032b2eaa1749..0c96a423b6deb 100644 --- a/packages/create-next-app/index.ts +++ b/packages/create-next-app/index.ts @@ -4,12 +4,11 @@ import Commander from 'commander' import path from 'path' import prompts from 'prompts' import checkForUpdate from 'update-check' - -import { createApp } from './create-app' +import { createApp, DownloadError } from './create-app' +import { listExamples } from './helpers/examples' +import { shouldUseYarn } from './helpers/should-use-yarn' import { validateNpmName } from './helpers/validate-pkg' import packageJson from './package.json' -import { shouldUseYarn } from './helpers/should-use-yarn' -import { listExamples } from './helpers/examples' let projectPath: string = '' @@ -166,12 +165,32 @@ async function run(): Promise { } const example = typeof program.example === 'string' && program.example.trim() - await createApp({ - appPath: resolvedProjectPath, - useNpm: !!program.useNpm, - example: example && example !== 'default' ? example : undefined, - examplePath: program.examplePath, - }) + try { + await createApp({ + appPath: resolvedProjectPath, + useNpm: !!program.useNpm, + example: example && example !== 'default' ? example : undefined, + examplePath: program.examplePath, + }) + } catch (reason) { + if (!(reason instanceof DownloadError)) { + throw reason + } + + const res = await prompts({ + type: 'confirm', + name: 'builtin', + message: + `Could not download "${example}" because of a connectivity issue between your machine and GitHub.\n` + + `Do you want to use the default template instead?`, + initial: true, + }) + if (!res.builtin) { + throw reason + } + + await createApp({ appPath: resolvedProjectPath, useNpm: !!program.useNpm }) + } } const update = checkForUpdate(packageJson).catch(() => null) diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index e45fe2c87a082..6eb4a5514d2f2 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -27,12 +27,14 @@ "prepublish": "yarn release" }, "devDependencies": { + "@types/async-retry": "1.4.2", "@types/node": "^12.6.8", "@types/prompts": "2.0.1", "@types/rimraf": "3.0.0", "@types/tar": "4.0.3", "@types/validate-npm-package-name": "3.0.0", "@zeit/ncc": "^0.20.4", + "async-retry": "1.3.1", "chalk": "2.4.2", "commander": "2.20.0", "cpy": "7.3.0", diff --git a/test/integration/create-next-app/index.test.js b/test/integration/create-next-app/index.test.js index dc907927af429..71ed454cf4eeb 100644 --- a/test/integration/create-next-app/index.test.js +++ b/test/integration/create-next-app/index.test.js @@ -1,12 +1,12 @@ /* eslint-env jest */ -import path from 'path' -import fs from 'fs-extra' import execa from 'execa' +import fs from 'fs-extra' import os from 'os' +import path from 'path' const cli = require.resolve('create-next-app/dist/index.js') -jest.setTimeout(1000 * 60 * 2) +jest.setTimeout(1000 * 60 * 5) const run = (cwd, ...args) => execa('node', [cli, ...args], { cwd }) const runStarter = (cwd, ...args) => { @@ -229,6 +229,36 @@ describe('create next app', () => { expect(res.stdout).toMatch(/Downloading files for example hello-world/) }) }) + + it('should fall back to default template', async () => { + await usingTempDir(async (cwd) => { + const runExample = (...args) => { + const res = run(cwd, ...args) + + function fallbackToTemplate(data) { + if ( + /Do you want to use the default template instead/.test( + data.toString() + ) + ) { + res.stdout.removeListener('data', fallbackToTemplate) + res.stdin.write('\n') + } + } + + res.stdout.on('data', fallbackToTemplate) + + return res + } + + const res = await runExample( + 'fail-example', + '--example', + '__internal-testing-retry' + ) + expect(res.exitCode).toBe(0) + }) + }) } it('should allow an example named default', async () => { diff --git a/yarn.lock b/yarn.lock index 7863bd8b7513c..300aab2a1a904 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2407,6 +2407,13 @@ version "1.3.1" resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" +"@types/async-retry@1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.4.2.tgz#7f910188cd3893b51e32df51765ee8d5646053e3" + integrity sha512-GUDuJURF0YiJZ+CBjNQA0+vbP/VHlJbB0sFqkzsV7EcOPRfurVonXpXKAt3w8qIjM1TEzpz6hc6POocPvHOS3w== + dependencies: + "@types/retry" "*" + "@types/babel__code-frame@7.0.1": version "7.0.1" resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.1.tgz#baf2529c4abbfb5e4008c845efcfe39a187e2f99" @@ -2778,6 +2785,11 @@ dependencies: "@types/node" "*" +"@types/retry@*": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + "@types/rimraf@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.0.tgz#b9d03f090ece263671898d57bb7bb007023ac19f" @@ -3585,6 +3597,13 @@ async-retry@1.2.3: dependencies: retry "0.12.0" +async-retry@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55" + integrity sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA== + dependencies: + retry "0.12.0" + async-sema@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/async-sema/-/async-sema-3.0.0.tgz#9e22d6783f0ab66a1cf330e21a905e39b3b3a975"