Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Confirm if user wants example when creating app #10543

Merged
merged 17 commits into from
Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/create-next-app/helpers/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,10 @@ export function downloadAndExtractExample(
tar.extract({ cwd: root, strip: 3 }, [`next.js-canary/examples/${name}`])
)
}

export async function listExamples(): Promise<any> {
const res = await got(
'https://api.github.com/repositories/70107786/contents/examples'
)
return JSON.parse(res.body)
}
70 changes: 69 additions & 1 deletion packages/create-next-app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''

Expand All @@ -21,7 +22,7 @@ const program = new Commander.Command(packageJson.name)
})
.option('--use-npm')
.option(
'-e, --example <name>|<github-url>',
'-e, --example [name]|[github-url]',
`

An example to bootstrap the app with. You can use an example name
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -130,6 +197,7 @@ async function notifyUpdate() {
)
console.log()
}
process.exit()
} catch {
// ignore error
}
Expand Down
58 changes: 54 additions & 4 deletions test/integration/create-next-app/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,43 @@ 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/)
}
})

it('empty directory', async () => {
const projectName = 'empty-directory'
const res = await run(projectName)
const res = await runStarter(projectName)

expect(res.exitCode).toBe(0)
expect(
Expand Down Expand Up @@ -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/)
})
})