From dcf2cf3e1233cd8fcd7798ee2b976a0a59c40855 Mon Sep 17 00:00:00 2001 From: Marco Moretti Date: Sat, 25 Apr 2020 15:33:14 +0200 Subject: [PATCH 1/3] [create-next-app] Permit user to retry to download example files --- packages/create-next-app/create-app.ts | 37 ++++++++++++---------- packages/create-next-app/index.ts | 44 +++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index 27835c6c9faaa..4824eba3c46cd 100644 --- a/packages/create-next-app/create-app.ts +++ b/packages/create-next-app/create-app.ts @@ -109,24 +109,27 @@ 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) { + 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) + } + } catch (reason) { + throw new Error(`Cannot download files for ${example}`) } - // 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/index.ts b/packages/create-next-app/index.ts index 1385357a705ff..df3876ed0ebc6 100644 --- a/packages/create-next-app/index.ts +++ b/packages/create-next-app/index.ts @@ -43,6 +43,48 @@ const program = new Commander.Command(packageJson.name) .allowUnknownOption() .parse(process.argv) +async function tryCreateApp({ + appPath, + useNpm, + example, + examplePath, +}: { + appPath: string + useNpm: boolean + example?: string + examplePath?: string +}) { + try { + await createApp({ + appPath: appPath, + useNpm: useNpm, + example: example, + examplePath: examplePath, + }) + } catch (reason) { + if (reason.command) { + throw reason + } else { + const res = await prompts({ + type: 'confirm', + name: 'retry', + message: 'An error occured. Do you want to retry?', + initial: false, + }) + if (res.retry) { + await tryCreateApp({ + appPath, + useNpm, + example, + examplePath, + }) + } else { + throw reason + } + } + } +} + async function run() { if (typeof projectPath === 'string') { projectPath = projectPath.trim() @@ -165,7 +207,7 @@ async function run() { } } - await createApp({ + await tryCreateApp({ appPath: resolvedProjectPath, useNpm: !!program.useNpm, example: From 3591ac1151180f09e53365940c4cc5ffa4f688ad Mon Sep 17 00:00:00 2001 From: marcosvega91 Date: Sun, 26 Apr 2020 19:36:36 +0200 Subject: [PATCH 2/3] Add auto retry for creating app If there is an error downloading example files, the programm will try 3 times. After that the user can choose to use the default templace instead of the example one. --- packages/create-next-app/create-app.ts | 10 ++++++++- packages/create-next-app/index.ts | 31 ++++++++++++++++++-------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index 4824eba3c46cd..a77fdb6de5de4 100644 --- a/packages/create-next-app/create-app.ts +++ b/packages/create-next-app/create-app.ts @@ -19,6 +19,14 @@ 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 { + example: string + constructor(message: string, example: string) { + super(message) + this.example = example + } +} + export async function createApp({ appPath, useNpm, @@ -128,7 +136,7 @@ export async function createApp({ await downloadAndExtractExample(root, example) } } catch (reason) { - throw new Error(`Cannot download files for ${example}`) + throw new DownloadError(`Cannot download files for ${example}`, example) } // Copy our default `.gitignore` if the application did not provide one const ignorePath = path.join(root, '.gitignore') diff --git a/packages/create-next-app/index.ts b/packages/create-next-app/index.ts index df3876ed0ebc6..95b812692be2c 100644 --- a/packages/create-next-app/index.ts +++ b/packages/create-next-app/index.ts @@ -5,7 +5,7 @@ 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 { validateNpmName } from './helpers/validate-pkg' import packageJson from './package.json' import { shouldUseYarn } from './helpers/should-use-yarn' @@ -48,12 +48,16 @@ async function tryCreateApp({ useNpm, example, examplePath, + attempt = 1, }: { appPath: string useNpm: boolean example?: string examplePath?: string + attempt?: number }) { + console.log() + console.log(`Trying to create project (${attempt} attempt)........`) try { await createApp({ appPath: appPath, @@ -65,18 +69,28 @@ async function tryCreateApp({ if (reason.command) { throw reason } else { - const res = await prompts({ - type: 'confirm', - name: 'retry', - message: 'An error occured. Do you want to retry?', - initial: false, - }) - if (res.retry) { + if (attempt === 3 && reason instanceof DownloadError) { + const res = await prompts({ + type: 'confirm', + name: 'tryDefault', + message: `Could not download example ${reason.example} because of a network error, do you want to use the default template instead?`, + initial: false, + }) + if (res.tryDefault) { + await tryCreateApp({ + appPath, + useNpm, + }) + } else { + throw reason + } + } else if (reason instanceof DownloadError) { await tryCreateApp({ appPath, useNpm, example, examplePath, + attempt: ++attempt, }) } else { throw reason @@ -206,7 +220,6 @@ async function run() { } } } - await tryCreateApp({ appPath: resolvedProjectPath, useNpm: !!program.useNpm, From aacdfd1a541513ca8a130f0e1e03c9e49c034298 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 26 May 2020 12:22:38 -0400 Subject: [PATCH 3/3] Use library --- packages/create-next-app/create-app.ts | 29 +++--- packages/create-next-app/helpers/examples.ts | 4 + packages/create-next-app/index.ts | 95 ++++++------------- packages/create-next-app/package.json | 2 + .../integration/create-next-app/index.test.js | 36 ++++++- yarn.lock | 19 ++++ 6 files changed, 101 insertions(+), 84 deletions(-) diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index a7250164b9ade..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,13 +19,7 @@ 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 { - example: string - constructor(message: string, example: string) { - super(message) - this.example = example - } -} +export class DownloadError extends Error {} export async function createApp({ appPath, @@ -83,7 +77,7 @@ export async function createApp({ ) process.exit(1) } - } else { + } else if (example !== '__internal-testing-retry') { const found = await hasExample(example) if (!found) { @@ -119,13 +113,16 @@ export async function createApp({ if (example) { try { if (repoInfo) { + const repoInfo2 = repoInfo console.log( `Downloading files from repo ${chalk.cyan( example )}. This might take a moment.` ) console.log() - await downloadAndExtractRepo(root, repoInfo) + await retry(() => downloadAndExtractRepo(root, repoInfo2), { + retries: 3, + }) } else { console.log( `Downloading files for example ${chalk.cyan( @@ -133,10 +130,12 @@ export async function createApp({ )}. This might take a moment.` ) console.log() - await downloadAndExtractExample(root, example) + await retry(() => downloadAndExtractExample(root, example), { + retries: 3, + }) } } catch (reason) { - throw new DownloadError(`Cannot download files for ${example}`, example) + throw new DownloadError(reason) } // Copy our default `.gitignore` if the application did not provide one const ignorePath = path.join(root, '.gitignore') 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 097ac4f55e5c6..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, 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 = '' @@ -43,63 +42,7 @@ const program = new Commander.Command(packageJson.name) .allowUnknownOption() .parse(process.argv) -async function tryCreateApp({ - appPath, - useNpm, - example, - examplePath, - attempt = 1, -}: { - appPath: string - useNpm: boolean - example?: string - examplePath?: string - attempt?: number -}) { - console.log() - console.log(`Trying to create project (${attempt} attempt)........`) - try { - await createApp({ - appPath: appPath, - useNpm: useNpm, - example: example, - examplePath: examplePath, - }) - } catch (reason) { - if (reason.command) { - throw reason - } else { - if (attempt === 3 && reason instanceof DownloadError) { - const res = await prompts({ - type: 'confirm', - name: 'tryDefault', - message: `Could not download example ${reason.example} because of a network error, do you want to use the default template instead?`, - initial: false, - }) - if (res.tryDefault) { - await tryCreateApp({ - appPath, - useNpm, - }) - } else { - throw reason - } - } else if (reason instanceof DownloadError) { - await tryCreateApp({ - appPath, - useNpm, - example, - examplePath, - attempt: ++attempt, - }) - } else { - throw reason - } - } - } -} - -async function run() { +async function run(): Promise { if (typeof projectPath === 'string') { projectPath = projectPath.trim() } @@ -222,12 +165,32 @@ async function run() { } const example = typeof program.example === 'string' && program.example.trim() - await tryCreateApp({ - 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"