Skip to content

Commit

Permalink
feat(create-next-app): JS/TS prompt (with appDir support); enhanced t…
Browse files Browse the repository at this point in the history
…esting (#42012)

Resubmitting this PR following this comment from Tim. This work has been done already and we can build off of it to get this in faster. 

> Just to be clear we're planning to rework create-next-app to give you the option to choose between JavaScript or TypeScript so it'll solve this request. For `app` right now it'll stay TypeScript till that is implemented.
> 
> _Originally posted by @timneutkens in #41745 (reply in thread)

---

I added the `--ts, --typescript` flag to `create-next-app` in #24655, and since then I have seen it used very frequently, including in recent issues (such as #33314) and most Next.js tutorials on YouTube. I noticed the template logic added in this PR was also used to add the `appDir` configuration as well.

We discussed a while ago adding the following user flow:

- `create-next-app --js, --javascript` creates a JS project
- `create-next-app --ts, --typescript` creates a TS project
- `create-next-app [name]` (no `--js, --ts`) prompts the user to choose either JS or TS, with TS selected by default.

### Review

Adding support for appDir and refactoring the templates brought the pain-of-review up a bit, but it's not so bad when broken into increments.

The original 8-file diff is here:
ctjlewis@1f47d9b

And the PR that brought the diff up to 59 files (mostly owed to `app` template dirs and file structure refactors):
ctjlewis@bd3ae4a ([PR link](https://github.com/ctjlewis/next.js/pull/3/files))

### Demo

https://user-images.githubusercontent.com/1657236/198586216-4691ff4c-48d4-4c6c-b7c1-705c38dd0194.mov


Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
  • Loading branch information
ctjlewis and ijjk committed Oct 31, 2022
1 parent b7a9b06 commit 389c77f
Show file tree
Hide file tree
Showing 61 changed files with 1,210 additions and 432 deletions.
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>
)
}
5 changes: 5 additions & 0 deletions packages/create-next-app/templates/app/js/pages/api/hello.js
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' })
}

1 comment on commit 389c77f

@tmadeira
Copy link

@tmadeira tmadeira commented on 389c77f Nov 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! Just a heads-up: create-next-app --ts is not working in CI environments (#42592); the issue seems related to this patch. (@ctjlewis @ijjk @timneutkens)

Please sign in to comment.