diff --git a/.eslintignore b/.eslintignore index 84c23fbd68..b080c941c5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -17,6 +17,7 @@ packages/next-codemod/transforms/__tests__/**/* packages/next-codemod/**/*.js packages/next-codemod/**/*.d.ts packages/next-env/**/*.d.ts +packages/create-next-app/templates/** test/integration/async-modules/** test/integration/eslint/** test-timings.json diff --git a/docs/getting-started.md b/docs/getting-started.md index 6de8da2f92..9c7d752b89 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -27,6 +27,14 @@ npx create-next-app yarn create next-app ``` +If you want to start with a TypeScript project you can use the `--typescript` flag: + +```bash +npx create-next-app --typescript +# or +yarn create next-app --typescript +``` + After the installation is complete, follow the instructions to start the development server. Try editing `pages/index.js` and see the result on your browser. For more information on how to use `create-next-app`, you can review the [`create-next-app` documentation](/docs/api-reference/create-next-app.md) diff --git a/packages/create-next-app/README.md b/packages/create-next-app/README.md index 35ff933409..deeb80d655 100644 --- a/packages/create-next-app/README.md +++ b/packages/create-next-app/README.md @@ -16,6 +16,7 @@ npx create-next-app blog-app `create-next-app` comes with the following options: +- **--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/master/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. Yarn will be used by default if it's installed diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index 2db10c89c0..577ed31e1f 100644 --- a/packages/create-next-app/create-app.ts +++ b/packages/create-next-app/create-app.ts @@ -28,13 +28,16 @@ export async function createApp({ useNpm, example, examplePath, + typescript, }: { appPath: string useNpm: boolean example?: string examplePath?: string + typescript?: boolean }): Promise { let repoInfo: RepoInfo | undefined + const template = typescript ? 'typescript' : 'default' if (example) { let repoUrl: URL | undefined @@ -124,6 +127,9 @@ export async function createApp({ process.chdir(root) if (example) { + /** + * If an example repository is provided, clone it. + */ try { if (repoInfo) { const repoInfo2 = repoInfo @@ -154,7 +160,7 @@ export async function createApp({ const ignorePath = path.join(root, '.gitignore') if (!fs.existsSync(ignorePath)) { fs.copyFileSync( - path.join(__dirname, 'templates', 'default', 'gitignore'), + path.join(__dirname, 'templates', template, 'gitignore'), ignorePath ) } @@ -165,30 +171,83 @@ export async function createApp({ await install(root, null, { useYarn, isOnline }) console.log() } else { + /** + * Otherwise, if an example repository is not provided for cloning, proceed + * by installing from a template. + */ + console.log(chalk.bold(`Using ${displayedCommand}.`)) + /** + * 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' }, + scripts: { + dev: 'next dev', + build: 'next build', + start: 'next start', + }, } + /** + * 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 = { useYarn, isOnline } + /** + * Default dependencies. + */ + const dependencies = ['react', 'react-dom', 'next'] + /** + * Default devDependencies. + */ + const devDependencies = [] + /** + * TypeScript projects will have type definitions and other devDependencies. + */ + if (typescript) { + devDependencies.push('typescript', '@types/react', '@types/next') + } + /** + * 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() - console.log( - `Installing ${chalk.cyan('react')}, ${chalk.cyan( - 'react-dom' - )}, and ${chalk.cyan('next')} using ${displayedCommand}...` - ) + 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() - - await install(root, ['react', 'react-dom', 'next'], { useYarn, isOnline }) - console.log() - + /** + * Copy the template files to the target directory. + */ await cpy('**', root, { parents: true, - cwd: path.join(__dirname, 'templates', 'default'), + cwd: path.join(__dirname, 'templates', template), rename: (name) => { switch (name) { case 'gitignore': { diff --git a/packages/create-next-app/helpers/install.ts b/packages/create-next-app/helpers/install.ts index 872c8e0be2..f1c814f58d 100644 --- a/packages/create-next-app/helpers/install.ts +++ b/packages/create-next-app/helpers/install.ts @@ -2,41 +2,98 @@ import chalk from 'chalk' import spawn from 'cross-spawn' +interface InstallArgs { + /** + * Indicate whether to install packages using Yarn. + */ + useYarn: boolean + /** + * Indicate whether there is an active Internet connection. + */ + isOnline: boolean + /** + * Indicate whether the given dependencies are devDependencies. + */ + devDependencies?: boolean +} + +/** + * Spawn a package manager installation with either Yarn or NPM. + * + * @returns A Promise that resolves once the installation is finished. + */ export function install( root: string, dependencies: string[] | null, - { useYarn, isOnline }: { useYarn: boolean; isOnline: boolean } + { useYarn, isOnline, devDependencies }: InstallArgs ): Promise { + /** + * NPM-specific command-line flags. + */ + const npmFlags: string[] = ['--logLevel', 'error'] + /** + * Yarn-specific command-line flags. + */ + const yarnFlags: string[] = [] + /** + * Return a Promise that resolves once the installation is finished. + */ return new Promise((resolve, reject) => { - let command: string let args: string[] - if (useYarn) { - command = 'yarnpkg' - args = dependencies ? ['add', '--exact'] : ['install'] - if (!isOnline) { - args.push('--offline') - } - if (dependencies) { + let command: string = useYarn ? 'yarnpkg' : 'npm' + + if (dependencies && dependencies.length) { + /** + * If there are dependencies, run a variation of `{displayCommand} add`. + */ + if (useYarn) { + /** + * Call `yarn add --exact (--offline)? (-D)? ...`. + */ + args = ['add', '--exact'] + if (!isOnline) args.push('--offline') + args.push('--cwd', root) + if (devDependencies) args.push('--dev') + args.push(...dependencies) + } else { + /** + * Call `npm install [--save|--save-dev] ...`. + */ + args = ['install', '--save-exact'] + args.push(devDependencies ? '--save-dev' : '--save') args.push(...dependencies) } - args.push('--cwd', root) - - if (!isOnline) { - console.log(chalk.yellow('You appear to be offline.')) - console.log(chalk.yellow('Falling back to the local Yarn cache.')) - console.log() - } } else { - command = 'npm' - args = ([ - 'install', - dependencies && '--save', - dependencies && '--save-exact', - '--loglevel', - 'error', - ].filter(Boolean) as string[]).concat(dependencies || []) + /** + * If there are no dependencies, run a variation of `{displayCommand} + * install`. + */ + args = ['install'] + if (useYarn) { + if (!isOnline) { + console.log(chalk.yellow('You appear to be offline.')) + console.log(chalk.yellow('Falling back to the local Yarn cache.')) + console.log() + args.push('--offline') + } + } else { + if (!isOnline) { + console.log(chalk.yellow('You appear to be offline.')) + console.log() + } + } } - + /** + * Add any package manager-specific flags. + */ + if (useYarn) { + args.push(...yarnFlags) + } else { + args.push(...npmFlags) + } + /** + * Spawn the installation process. + */ const child = spawn(command, args, { stdio: 'inherit', env: { ...process.env, ADBLOCK: '1', DISABLE_OPENCOLLECTIVE: '1' }, diff --git a/packages/create-next-app/index.ts b/packages/create-next-app/index.ts index ab7431fc3b..a947521711 100644 --- a/packages/create-next-app/index.ts +++ b/packages/create-next-app/index.ts @@ -19,7 +19,20 @@ const program = new Commander.Command(packageJson.name) .action((name) => { projectPath = name }) - .option('--use-npm', 'Explicitly tell the CLI to bootstrap the app using npm') + .option( + '--ts, --typescript', + ` + + Initialize as a TypeScript project. +` + ) + .option( + '--use-npm', + ` + + Explicitly tell the CLI to bootstrap the app using npm +` + ) .option( '-e, --example [name]|[github-url]', ` @@ -113,6 +126,7 @@ async function run(): Promise { useNpm: !!program.useNpm, example: example && example !== 'default' ? example : undefined, examplePath: program.examplePath, + typescript: program.typescript, }) } catch (reason) { if (!(reason instanceof DownloadError)) { @@ -131,7 +145,11 @@ async function run(): Promise { throw reason } - await createApp({ appPath: resolvedProjectPath, useNpm: !!program.useNpm }) + await createApp({ + appPath: resolvedProjectPath, + useNpm: !!program.useNpm, + typescript: program.typescript, + }) } } diff --git a/packages/create-next-app/templates/typescript/README-template.md b/packages/create-next-app/templates/typescript/README-template.md new file mode 100644 index 0000000000..b12f3e33e7 --- /dev/null +++ b/packages/create-next-app/templates/typescript/README-template.md @@ -0,0 +1,34 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/packages/create-next-app/templates/typescript/gitignore b/packages/create-next-app/templates/typescript/gitignore new file mode 100644 index 0000000000..1437c53f70 --- /dev/null +++ b/packages/create-next-app/templates/typescript/gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/packages/create-next-app/templates/typescript/next-env.d.ts b/packages/create-next-app/templates/typescript/next-env.d.ts new file mode 100644 index 0000000000..7b7aa2c772 --- /dev/null +++ b/packages/create-next-app/templates/typescript/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/packages/create-next-app/templates/typescript/pages/_app.tsx b/packages/create-next-app/templates/typescript/pages/_app.tsx new file mode 100644 index 0000000000..945e892613 --- /dev/null +++ b/packages/create-next-app/templates/typescript/pages/_app.tsx @@ -0,0 +1,7 @@ +import '../styles/globals.css' +import type { AppProps } from 'next/app' + +function MyApp({ Component, pageProps }: AppProps) { + return +} +export default MyApp diff --git a/packages/create-next-app/templates/typescript/pages/api/hello.ts b/packages/create-next-app/templates/typescript/pages/api/hello.ts new file mode 100644 index 0000000000..3d66af99d6 --- /dev/null +++ b/packages/create-next-app/templates/typescript/pages/api/hello.ts @@ -0,0 +1,10 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' + +type Data = { + name: string +} + +export default (req: NextApiRequest, res: NextApiResponse) => { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/packages/create-next-app/templates/typescript/pages/index.tsx b/packages/create-next-app/templates/typescript/pages/index.tsx new file mode 100644 index 0000000000..08145bba9a --- /dev/null +++ b/packages/create-next-app/templates/typescript/pages/index.tsx @@ -0,0 +1,69 @@ +import Head from 'next/head' +import Image from 'next/image' +import styles from '../styles/Home.module.css' + +export default function Home() { + return ( + + ) +} diff --git a/packages/create-next-app/templates/typescript/public/favicon.ico b/packages/create-next-app/templates/typescript/public/favicon.ico new file mode 100644 index 0000000000..4965832f2c Binary files /dev/null and b/packages/create-next-app/templates/typescript/public/favicon.ico differ diff --git a/packages/create-next-app/templates/typescript/public/vercel.svg b/packages/create-next-app/templates/typescript/public/vercel.svg new file mode 100644 index 0000000000..fbf0e25a65 --- /dev/null +++ b/packages/create-next-app/templates/typescript/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/create-next-app/templates/typescript/styles/Home.module.css b/packages/create-next-app/templates/typescript/styles/Home.module.css new file mode 100644 index 0000000000..35454bb748 --- /dev/null +++ b/packages/create-next-app/templates/typescript/styles/Home.module.css @@ -0,0 +1,121 @@ +.container { + min-height: 100vh; + padding: 0 0.5rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; +} + +.main { + padding: 5rem 0; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.footer { + width: 100%; + height: 100px; + border-top: 1px solid #eaeaea; + display: flex; + justify-content: center; + align-items: center; +} + +.footer a { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; +} + +.title a { + color: #0070f3; + text-decoration: none; +} + +.title a:hover, +.title a:focus, +.title a:active { + text-decoration: underline; +} + +.title { + margin: 0; + line-height: 1.15; + font-size: 4rem; +} + +.title, +.description { + text-align: center; +} + +.description { + line-height: 1.5; + font-size: 1.5rem; +} + +.code { + background: #fafafa; + border-radius: 5px; + padding: 0.75rem; + font-size: 1.1rem; + font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, + Bitstream Vera Sans Mono, Courier New, monospace; +} + +.grid { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + max-width: 800px; + margin-top: 3rem; +} + +.card { + margin: 1rem; + padding: 1.5rem; + text-align: left; + color: inherit; + text-decoration: none; + border: 1px solid #eaeaea; + border-radius: 10px; + transition: color 0.15s ease, border-color 0.15s ease; + width: 45%; +} + +.card:hover, +.card:focus, +.card:active { + color: #0070f3; + border-color: #0070f3; +} + +.card h2 { + margin: 0 0 1rem 0; + font-size: 1.5rem; +} + +.card p { + margin: 0; + font-size: 1.25rem; + line-height: 1.5; +} + +.logo { + height: 1em; + margin-left: 0.5rem; +} + +@media (max-width: 600px) { + .grid { + width: 100%; + flex-direction: column; + } +} diff --git a/packages/create-next-app/templates/typescript/styles/globals.css b/packages/create-next-app/templates/typescript/styles/globals.css new file mode 100644 index 0000000000..e5e2dcc23b --- /dev/null +++ b/packages/create-next-app/templates/typescript/styles/globals.css @@ -0,0 +1,16 @@ +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} diff --git a/packages/create-next-app/templates/typescript/tsconfig.json b/packages/create-next-app/templates/typescript/tsconfig.json new file mode 100644 index 0000000000..4fa631c261 --- /dev/null +++ b/packages/create-next-app/templates/typescript/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/create-next-app/tsconfig.json b/packages/create-next-app/tsconfig.json index d0e9e223e6..aad5b04667 100644 --- a/packages/create-next-app/tsconfig.json +++ b/packages/create-next-app/tsconfig.json @@ -6,5 +6,6 @@ "resolveJsonModule": true, "esModuleInterop": true, "skipLibCheck": false - } + }, + "exclude": ["templates", "dist"] } diff --git a/test/integration/create-next-app/index.test.js b/test/integration/create-next-app/index.test.js index 6f35a294b6..3e3c868076 100644 --- a/test/integration/create-next-app/index.test.js +++ b/test/integration/create-next-app/index.test.js @@ -91,6 +91,37 @@ describe('create next app', () => { }) }) + it('should support typescript flag', async () => { + await usingTempDir(async (cwd) => { + const projectName = 'typescript' + const res = await run([projectName, '--typescript'], { cwd }) + expect(res.exitCode).toBe(0) + + expect( + fs.existsSync(path.join(cwd, projectName, 'package.json')) + ).toBeTruthy() + expect( + fs.existsSync(path.join(cwd, projectName, 'pages/index.tsx')) + ).toBeTruthy() + expect( + fs.existsSync(path.join(cwd, projectName, 'pages/_app.tsx')) + ).toBeTruthy() + expect( + fs.existsSync(path.join(cwd, projectName, 'pages/api/hello.ts')) + ).toBeTruthy() + expect( + fs.existsSync(path.join(cwd, projectName, 'tsconfig.json')) + ).toBeTruthy() + expect( + fs.existsSync(path.join(cwd, projectName, 'next-env.d.ts')) + ).toBeTruthy() + // check we copied default `.gitignore` + expect( + fs.existsSync(path.join(cwd, projectName, '.gitignore')) + ).toBeTruthy() + }) + }) + it('should allow example with GitHub URL', async () => { await usingTempDir(async (cwd) => { const projectName = 'github-app'