From bc7d18367b6c2304a68542d83431b19999c77f90 Mon Sep 17 00:00:00 2001 From: Dylan Brookes <24858006+merelinguist@users.noreply.github.com> Date: Tue, 7 Apr 2020 18:11:29 +0100 Subject: [PATCH] Confirm if user wants example when creating app (#10543) * Confirm if user wants example when creating app * Only ask user to choose example if they provide --example flag * Simplify the search * Added test * Use a select to pick the template * Updated test * check for lowercase and updated no example message * Add message if no template is selected * Updated tests * Handle error if fetching the examples fails * Fixed tests * Updated test timeout * Update examples.ts Co-authored-by: merelinguist Co-authored-by: Luis Alvarez Co-authored-by: Tim Neutkens Co-authored-by: Joe Haddad --- packages/create-next-app/helpers/examples.ts | 7 ++ packages/create-next-app/index.ts | 70 ++++++++++++++++++- .../integration/create-next-app/index.test.js | 58 +++++++++++++-- 3 files changed, 130 insertions(+), 5 deletions(-) diff --git a/packages/create-next-app/helpers/examples.ts b/packages/create-next-app/helpers/examples.ts index 7b8f874db8ab2..9b1781b5b9f6e 100644 --- a/packages/create-next-app/helpers/examples.ts +++ b/packages/create-next-app/helpers/examples.ts @@ -86,3 +86,10 @@ export function downloadAndExtractExample( tar.extract({ cwd: root, strip: 3 }, [`next.js-canary/examples/${name}`]) ) } + +export async function listExamples(): Promise { + const res = await got( + 'https://api.github.com/repositories/70107786/contents/examples' + ) + return JSON.parse(res.body) +} diff --git a/packages/create-next-app/index.ts b/packages/create-next-app/index.ts index 326a0698a3c6c..1385357a705ff 100644 --- a/packages/create-next-app/index.ts +++ b/packages/create-next-app/index.ts @@ -9,6 +9,7 @@ import { createApp } from './create-app' 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 = '' @@ -21,7 +22,7 @@ const program = new Commander.Command(packageJson.name) }) .option('--use-npm') .option( - '-e, --example |', + '-e, --example [name]|[github-url]', ` An example to bootstrap the app with. You can use an example name @@ -98,6 +99,72 @@ async function run() { process.exit(1) } + if (!program.example) { + const template = await prompts({ + type: 'select', + name: 'value', + message: 'Pick a template', + choices: [ + { title: 'Default starter app', value: 'default' }, + { title: 'Example from the Next.js repo', value: 'example' }, + ], + }) + + if (!template.value) { + console.log() + console.log('Please specify the template') + process.exit(1) + } + + if (template.value === 'example') { + let examplesJSON: any + + try { + examplesJSON = await listExamples() + } catch (error) { + console.log() + console.log( + 'Failed to fetch the list of examples with the following error:' + ) + console.error(error) + console.log() + console.log('Switching to the default starter app') + console.log() + } + + if (examplesJSON) { + const choices = examplesJSON.map((example: any) => ({ + title: example.name, + value: example.name, + })) + // The search function built into `prompts` isn’t very helpful: + // someone searching for `styled-components` would get no results since + // the example is called `with-styled-components`, and `prompts` searches + // the beginnings of titles. + const nameRes = await prompts({ + type: 'autocomplete', + name: 'exampleName', + message: 'Pick an example', + choices, + suggest: (input: any, choices: any) => { + const regex = new RegExp(input, 'i') + return choices.filter((choice: any) => regex.test(choice.title)) + }, + }) + + if (!nameRes.exampleName) { + console.log() + console.log( + 'Please specify an example or use the default starter app.' + ) + process.exit(1) + } + + program.example = nameRes.exampleName + } + } + } + await createApp({ appPath: resolvedProjectPath, useNpm: !!program.useNpm, @@ -130,6 +197,7 @@ async function notifyUpdate() { ) console.log() } + process.exit() } catch { // ignore error } diff --git a/test/integration/create-next-app/index.test.js b/test/integration/create-next-app/index.test.js index c7bb146dd1b6d..6e9315dd75082 100644 --- a/test/integration/create-next-app/index.test.js +++ b/test/integration/create-next-app/index.test.js @@ -13,23 +13,35 @@ const cwd = path.join( ) const run = (...args) => execa('node', [cli, ...args], { cwd }) +const runStarter = (...args) => { + const res = run(...args) + + res.stdout.on('data', data => { + const stdout = data.toString() + + if (/Pick a template/.test(stdout)) { + res.stdin.write('\n') + } + }) + + return res +} describe('create next app', () => { beforeAll(async () => { - jest.setTimeout(1000 * 60) + jest.setTimeout(1000 * 60 * 2) await fs.mkdirp(cwd) }) it('non-empty directory', async () => { const projectName = 'non-empty-directory' - await fs.mkdirp(path.join(cwd, projectName)) const pkg = path.join(cwd, projectName, 'package.json') fs.writeFileSync(pkg, '{ "foo": "bar" }') expect.assertions(1) try { - await run(projectName) + await runStarter(projectName) } catch (e) { expect(e.stdout).toMatch(/contains files that could conflict/) } @@ -37,7 +49,7 @@ describe('create next app', () => { it('empty directory', async () => { const projectName = 'empty-directory' - const res = await run(projectName) + const res = await runStarter(projectName) expect(res.exitCode).toBe(0) expect( @@ -150,4 +162,42 @@ describe('create next app', () => { fs.existsSync(path.join(cwd, projectName, '.gitignore')) ).toBeTruthy() }) + + it('should allow to manually select an example', async () => { + const runExample = (...args) => { + const res = run(...args) + + function pickExample(data) { + if (/hello-world/.test(data.toString())) { + res.stdout.removeListener('data', pickExample) + res.stdin.write('\n') + } + } + + function searchExample(data) { + if (/Pick an example/.test(data.toString())) { + res.stdout.removeListener('data', searchExample) + res.stdin.write('hello-world') + res.stdout.on('data', pickExample) + } + } + + function selectExample(data) { + if (/Pick a template/.test(data.toString())) { + res.stdout.removeListener('data', selectExample) + res.stdin.write('\u001b[B\n') // Down key and enter + res.stdout.on('data', searchExample) + } + } + + res.stdout.on('data', selectExample) + + return res + } + + const res = await runExample('no-example') + + expect(res.exitCode).toBe(0) + expect(res.stdout).toMatch(/Downloading files for example hello-world/) + }) })