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

feat(create-next-app): interactive mode; --js, --ts with appDir support; enhanced testing #42012

Merged
merged 6 commits into from
Oct 31, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 45 additions & 14 deletions docs/api-reference/create-next-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ description: Create Next.js apps in one command with create-next-app.

The easiest way to get started with Next.js is by using `create-next-app`. This CLI tool enables you to quickly start building a new Next.js application, with everything set up for you. You can create a new app using the default Next.js template, or by using one of the [official Next.js examples](https://github.com/vercel/next.js/tree/canary/examples). To get started, use the following command:

### Interactive

You can create a new project interactively by running:

```bash
npx create-next-app@latest
# or
Expand All @@ -14,27 +18,54 @@ yarn create next-app
pnpm create next-app
```

You can create a [TypeScript project](https://github.com/vercel/next.js/blob/canary/docs/basic-features/typescript.md) with the `--ts, --typescript` flag:
You will be asked for the name of your project, and then whether you want to
create a TypeScript project:

```bash
npx create-next-app@latest --ts
# or
yarn create next-app --typescript
# or
pnpm create next-app --ts
✔ Would you like to use TypeScript with this project? … No / Yes
```

### Options
Select **Yes** to install the necessary types/dependencies and create a new TS project.

### Non-interactive

You can also pass command line arguments to set up a new project
non-interactively. See `create-next-app --help`:

```bash
create-next-app <project-directory> [options]

Options:
-V, --version output the version number
--ts, --typescript

`create-next-app` comes with the following options:
Initialize as a TypeScript project. (default)

- **--ts, --typescript** - Initialize as a TypeScript project.
- **-e, --example [name]|[github-url]** - An example to bootstrap the app with. You can use an example name from the [Next.js repo](https://github.com/vercel/next.js/tree/canary/examples) or a GitHub URL. The URL can use any branch and/or subdirectory.
- **--example-path [path-to-example]** - In a rare case, your GitHub URL might contain a branch name with a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). In this case, you must specify the path to the example separately: `--example-path foo/bar`
- **--use-npm** - Explicitly tell the CLI to bootstrap the app using npm
- **--use-pnpm** - Explicitly tell the CLI to bootstrap the app using pnpm
--js, --javascript

Note: To bootstrap using `yarn` we recommend running `yarn create next-app`
Initialize as a JavaScript project.

--use-npm

Explicitly tell the CLI to bootstrap the app using npm

--use-pnpm

Explicitly tell the CLI to bootstrap the app using pnpm

-e, --example [name]|[github-url]

An example to bootstrap the app with. You can use an example name
from the official Next.js repo or a GitHub URL. The URL can use
any branch and/or subdirectory

--example-path <path-to-example>

In a rare case, your GitHub URL might contain a branch name with
a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar).
In this case, you must specify the path to the example separately:
--example-path foo/bar
```

### Why use Create Next App?

Expand Down
128 changes: 21 additions & 107 deletions packages/create-next-app/create-app.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import retry from 'async-retry'
import chalk from 'chalk'
import cpy from 'cpy'
import fs from 'fs'
import os from 'os'
import path from 'path'
import {
downloadAndExtractExample,
Expand All @@ -21,6 +19,13 @@ import { getOnline } from './helpers/is-online'
import { isWriteable } from './helpers/is-writeable'
import type { PackageManager } from './helpers/get-pkg-manager'

import {
getTemplateFile,
installTemplate,
TemplateMode,
TemplateType,
} from './templates'

export class DownloadError extends Error {}

export async function createApp({
Expand All @@ -39,11 +44,8 @@ export async function createApp({
experimentalApp: boolean
}): Promise<void> {
let repoInfo: RepoInfo | undefined
const template = experimentalApp
? 'experimental-app'
: typescript
? 'typescript'
: 'default'
const mode: TemplateMode = typescript ? 'ts' : 'js'
const template: TemplateType = experimentalApp ? 'app' : 'default'

if (example) {
let repoUrl: URL | undefined
Expand Down Expand Up @@ -176,20 +178,20 @@ export async function createApp({
isErrorLike(reason) ? reason.message : reason + ''
)
}
// Copy our default `.gitignore` if the application did not provide one
// Copy `.gitignore` if the application did not provide one
const ignorePath = path.join(root, '.gitignore')
if (!fs.existsSync(ignorePath)) {
fs.copyFileSync(
path.join(__dirname, 'templates', template, 'gitignore'),
getTemplateFile({ template, mode, file: 'gitignore' }),
ignorePath
)
}

// Copy default `next-env.d.ts` to any example that is typescript
// Copy `next-env.d.ts` to any example that is typescript
const tsconfigPath = path.join(root, 'tsconfig.json')
if (fs.existsSync(tsconfigPath)) {
fs.copyFileSync(
path.join(__dirname, 'templates', 'typescript', 'next-env.d.ts'),
getTemplateFile({ template, mode: 'ts', file: 'next-env.d.ts' }),
path.join(root, 'next-env.d.ts')
)
}
Expand All @@ -204,104 +206,16 @@ export async function createApp({
}
} else {
/**
* Otherwise, if an example repository is not provided for cloning, proceed
* If an example repository is not provided for cloning, proceed
* by installing from a template.
*/
console.log(chalk.bold(`Using ${packageManager}.`))
/**
* Create a package.json for the new project.
*/
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
scripts: {
dev: 'next dev',
build: 'next build',
start: 'next start',
lint: 'next lint',
},
}
/**
* Write it to disk.
*/
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
)
/**
* These flags will be passed to `install()`.
*/
const installFlags = { packageManager, isOnline }
/**
* Default dependencies.
*/
const dependencies = ['react', 'react-dom', 'next']
/**
* Default devDependencies.
*/
const devDependencies = ['eslint', 'eslint-config-next']
/**
* TypeScript projects will have type definitions and other devDependencies.
*/
if (template !== 'default') {
devDependencies.push(
'typescript',
'@types/react',
'@types/node',
'@types/react-dom'
)
}
/**
* Install package.json dependencies if they exist.
*/
if (dependencies.length) {
console.log()
console.log('Installing dependencies:')
for (const dependency of dependencies) {
console.log(`- ${chalk.cyan(dependency)}`)
}
console.log()

await install(root, dependencies, installFlags)
}
/**
* Install package.json devDependencies if they exist.
*/
if (devDependencies.length) {
console.log()
console.log('Installing devDependencies:')
for (const devDependency of devDependencies) {
console.log(`- ${chalk.cyan(devDependency)}`)
}
console.log()

const devInstallFlags = { devDependencies: true, ...installFlags }
await install(root, devDependencies, devInstallFlags)
}
console.log('\nInitializing project with template: ', template, '\n')
/**
* Copy the template files to the target directory.
*/
await cpy('**', root, {
parents: true,
cwd: path.join(__dirname, 'templates', template),
rename: (name) => {
switch (name) {
case 'gitignore':
case 'eslintrc.json': {
return '.'.concat(name)
}
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
case 'README-template.md': {
return 'README.md'
}
default: {
return name
}
}
},
await installTemplate({
appName,
root,
template,
mode,
packageManager,
isOnline,
})
}

Expand Down
54 changes: 53 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, DownloadError } from './create-app'
import { getPkgManager } from './helpers/get-pkg-manager'
import { validateNpmName } from './helpers/validate-pkg'
import packageJson from './package.json'
import ciInfo from 'ci-info'

let projectPath: string = ''

Expand All @@ -23,7 +24,14 @@ const program = new Commander.Command(packageJson.name)
'--ts, --typescript',
`

Initialize as a TypeScript project.
Initialize as a TypeScript project. (default)
`
)
.option(
'--js, --javascript',
`

Initialize as a JavaScript project.
`
)
.option(
Expand Down Expand Up @@ -136,6 +144,50 @@ async function run(): Promise<void> {
}

const example = typeof program.example === 'string' && program.example.trim()

/**
* If the user does not provide the necessary flags, prompt them for whether
* to use TS or JS.
*
* @todo Allow appDir to support TS or JS, currently TS-only and disables all
* --ts, --js features.
*/
if (!example && !program.typescript && !program.javascript) {
if (ciInfo.isCI) {
// default to JavaScript in CI as we can't prompt to
// prevent breaking setup flows
program.javascript = true
program.typescript = false
} else {
const styledTypeScript = chalk.hex('#007acc')('TypeScript')
const { typescript } = await prompts(
{
type: 'toggle',
name: 'typescript',
message: `Would you like to use ${styledTypeScript} with this project?`,
initial: true,
active: 'Yes',
inactive: 'No',
},
{
/**
* User inputs Ctrl+C or Ctrl+D to exit the prompt. We should close the
* process and not write to the file system.
*/
onCancel: () => {
console.error('Exiting.')
process.exit(1)
},
}
)
/**
* Depending on the prompt response, set the appropriate program flags.
*/
program.typescript = Boolean(typescript)
program.javascript = !Boolean(typescript)
}
}

try {
await createApp({
appPath: resolvedProjectPath,
Expand Down
2 changes: 2 additions & 0 deletions packages/create-next-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"devDependencies": {
"@types/async-retry": "1.4.2",
"@types/cross-spawn": "6.0.0",
"@types/ci-info": "2.0.0",
"@types/node": "^12.6.8",
"@types/prompts": "2.0.1",
"@types/rimraf": "3.0.0",
Expand All @@ -38,6 +39,7 @@
"@vercel/ncc": "0.34.0",
"async-retry": "1.3.1",
"chalk": "2.4.2",
"ci-info": "watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540",
"commander": "2.20.0",
"cpy": "7.3.0",
"cross-spawn": "6.0.5",
Expand Down
14 changes: 14 additions & 0 deletions packages/create-next-app/templates/app/js/app/layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import './globals.css'

export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</head>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction}

export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}