diff --git a/docs/api-reference/create-next-app.md b/docs/api-reference/create-next-app.md index f50da53ab3e14..3654c69a11d98 100644 --- a/docs/api-reference/create-next-app.md +++ b/docs/api-reference/create-next-app.md @@ -45,6 +45,18 @@ Options: Initialize as a JavaScript project. + --eslint + + Initialize with eslint config. + + --no-eslint + + Initialize without eslint config. + + --experimental-app + + Initialize as a `app/` directory project. + --use-npm Explicitly tell the CLI to bootstrap the app using npm diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index aeecf962e0e7a..0219c5be87287 100644 --- a/packages/create-next-app/create-app.ts +++ b/packages/create-next-app/create-app.ts @@ -34,6 +34,7 @@ export async function createApp({ example, examplePath, typescript, + eslint, experimentalApp, }: { appPath: string @@ -41,6 +42,7 @@ export async function createApp({ example?: string examplePath?: string typescript: boolean + eslint: boolean experimentalApp: boolean }): Promise { let repoInfo: RepoInfo | undefined @@ -216,6 +218,7 @@ export async function createApp({ mode, packageManager, isOnline, + eslint, }) } diff --git a/packages/create-next-app/index.ts b/packages/create-next-app/index.ts index 6c5f2d0577dba..71eb872de5a8e 100644 --- a/packages/create-next-app/index.ts +++ b/packages/create-next-app/index.ts @@ -32,6 +32,13 @@ const program = new Commander.Command(packageJson.name) ` Initialize as a JavaScript project. +` + ) + .option( + '--eslint', + ` + + Initialize with eslint config. ` ) .option( @@ -148,9 +155,6 @@ async function run(): Promise { /** * 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) { @@ -158,6 +162,7 @@ async function run(): Promise { // prevent breaking setup flows program.javascript = true program.typescript = false + program.eslint = false } else { const styledTypeScript = chalk.hex('#007acc')('TypeScript') const { typescript } = await prompts( @@ -180,6 +185,19 @@ async function run(): Promise { }, } ) + + if (!program.eslint) { + const styledEslint = chalk.hex('#007acc')('ESLint') + const { eslint } = await prompts({ + type: 'toggle', + name: 'eslint', + message: `Would you like to use ${styledEslint} with this project?`, + initial: false, + active: 'Yes', + inactive: 'No', + }) + program.eslint = Boolean(eslint) + } /** * Depending on the prompt response, set the appropriate program flags. */ @@ -195,6 +213,7 @@ async function run(): Promise { example: example && example !== 'default' ? example : undefined, examplePath: program.examplePath, typescript: program.typescript, + eslint: program.eslint, experimentalApp: program.experimentalApp, }) } catch (reason) { @@ -218,6 +237,7 @@ async function run(): Promise { appPath: resolvedProjectPath, packageManager, typescript: program.typescript, + eslint: program.eslint, experimentalApp: program.experimentalApp, }) } diff --git a/packages/create-next-app/templates/index.ts b/packages/create-next-app/templates/index.ts index e3c2a5fd42f59..0863fc87f46cb 100644 --- a/packages/create-next-app/templates/index.ts +++ b/packages/create-next-app/templates/index.ts @@ -29,6 +29,7 @@ export const installTemplate = async ({ isOnline, template, mode, + eslint, }: InstallTemplateArgs) => { console.log(chalk.bold(`Using ${packageManager}.`)) @@ -62,21 +63,24 @@ export const installTemplate = async ({ * 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 (mode === 'ts') { - devDependencies.push( + dependencies.push( 'typescript', '@types/react', '@types/node', '@types/react-dom' ) } + + /** + * Default eslint dependencies. + */ + if (eslint) { + dependencies.push('eslint', 'eslint-config-next') + } /** * Install package.json dependencies if they exist. */ @@ -90,24 +94,10 @@ export const installTemplate = async ({ 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) - } /** * Copy the template files to the target directory. */ - console.log('\nInitializing project with template: ', template, '\n') + console.log('\nInitializing project with template:', template, '\n') const templatePath = path.join(__dirname, template, mode) await cpy('**', root, { parents: true, @@ -129,6 +119,11 @@ export const installTemplate = async ({ } }, }) + + if (!eslint) { + // remove un-necessary template file if eslint is not desired + await fs.promises.unlink(path.join(root, '.eslintrc.json')) + } } export * from './types' diff --git a/packages/create-next-app/templates/types.ts b/packages/create-next-app/templates/types.ts index fae0e86cd651d..2a11359969d07 100644 --- a/packages/create-next-app/templates/types.ts +++ b/packages/create-next-app/templates/types.ts @@ -17,4 +17,5 @@ export interface InstallTemplateArgs { template: TemplateType mode: TemplateMode + eslint: boolean } diff --git a/test/integration/create-next-app/index.test.ts b/test/integration/create-next-app/index.test.ts index 18d4034792953..5b97b7821789a 100644 --- a/test/integration/create-next-app/index.test.ts +++ b/test/integration/create-next-app/index.test.ts @@ -36,7 +36,10 @@ describe('create next app', () => { const pkg = path.join(cwd, projectName, 'package.json') fs.writeFileSync(pkg, '{ "foo": "bar" }') - const res = await run([projectName, '--js'], { cwd, reject: false }) + const res = await run([projectName, '--js', '--eslint'], { + cwd, + reject: false, + }) expect(res.exitCode).toBe(1) expect(res.stdout).toMatch(/contains files that could conflict/) }) @@ -48,7 +51,7 @@ describe('create next app', () => { it('empty directory', async () => { await useTempDir(async (cwd) => { const projectName = 'empty-directory' - const res = await run([projectName, '--js'], { cwd }) + const res = await run([projectName, '--js', '--eslint'], { cwd }) expect(res.exitCode).toBe(0) shouldBeJavascriptProject({ cwd, projectName, template: 'default' }) @@ -60,7 +63,7 @@ describe('create next app', () => { await useTempDir(async (cwd) => { const projectName = 'invalid-example-name' const res = await run( - [projectName, '--js', '--example', 'not a real example'], + [projectName, '--js', '--eslint', '--example', 'not a real example'], { cwd, reject: false, @@ -79,9 +82,12 @@ describe('create next app', () => { it('valid example', async () => { await useTempDir(async (cwd) => { const projectName = 'valid-example' - const res = await run([projectName, '--js', '--example', 'basic-css'], { - cwd, - }) + const res = await run( + [projectName, '--js', '--eslint', '--example', 'basic-css'], + { + cwd, + } + ) expect(res.exitCode).toBe(0) projectFilesShouldExist({ cwd, @@ -100,7 +106,7 @@ describe('create next app', () => { await useTempDir(async (cwd) => { const projectName = 'valid-example-without-package-json' const res = await run( - [projectName, '--js', '--example', 'with-docker-compose'], + [projectName, '--js', '--eslint', '--example', 'with-docker-compose'], { cwd, } @@ -119,7 +125,13 @@ describe('create next app', () => { await useTempDir(async (cwd) => { const projectName = 'github-app' const res = await run( - [projectName, '--js', '--example', `${exampleRepo}/${examplePath}`], + [ + projectName, + '--js', + '--eslint', + '--example', + `${exampleRepo}/${examplePath}`, + ], { cwd, } @@ -146,6 +158,7 @@ describe('create next app', () => { [ projectName, '--js', + '--eslint', '--example', 'https://github.com/vercel/nextjs-portfolio-starter/', ], @@ -175,6 +188,7 @@ describe('create next app', () => { [ projectName, '--js', + '--eslint', '--example', exampleRepo, '--example-path', @@ -206,6 +220,7 @@ describe('create next app', () => { [ projectName, '--js', + '--eslint', '--example', `${exampleRepo}/${examplePath}`, '--example-path', @@ -237,7 +252,13 @@ describe('create next app', () => { await useTempDir(async (cwd) => { const projectName = 'fail-example' const res = await run( - [projectName, '--js', '--example', '__internal-testing-retry'], + [ + projectName, + '--js', + '--eslint', + '--example', + '__internal-testing-retry', + ], { cwd, input: '\n', @@ -253,9 +274,12 @@ describe('create next app', () => { it('should allow an example named default', async () => { await useTempDir(async (cwd) => { const projectName = 'default-example' - const res = await run([projectName, '--js', '--example', 'default'], { - cwd, - }) + const res = await run( + [projectName, '--js', '--eslint', '--example', 'default'], + { + cwd, + } + ) expect(res.exitCode).toBe(0) shouldBeJavascriptProject({ cwd, projectName, template: 'default' }) @@ -265,7 +289,7 @@ describe('create next app', () => { it('should exit if example flag is empty', async () => { await useTempDir(async (cwd) => { const projectName = 'no-example-provided' - const res = await run([projectName, '--js', '--example'], { + const res = await run([projectName, '--js', '--eslint', '--example'], { cwd, reject: false, }) @@ -277,7 +301,10 @@ describe('create next app', () => { it('should exit if the folder is not writable', async () => { await useTempDir(async (cwd) => { const projectName = 'not-writable' - const res = await run([projectName, '--js'], { cwd, reject: false }) + const res = await run([projectName, '--js', '--eslint'], { + cwd, + reject: false, + }) if (process.platform === 'win32') { expect(res.exitCode).toBe(0) @@ -311,7 +338,7 @@ describe('create next app', () => { delete env.npm_config_user_agent } - const res = await run(['.', '--js'], { + const res = await run(['.', '--js', '--eslint'], { cwd, env, extendEnv: false, @@ -327,7 +354,10 @@ describe('create next app', () => { it('should ask the user for a name for the project if none supplied', async () => { await useTempDir(async (cwd) => { const projectName = 'test-project' - const res = await run(['--js'], { cwd, input: `${projectName}\n` }) + const res = await run(['--js', '--eslint'], { + cwd, + input: `${projectName}\n`, + }) expect(res.exitCode).toBe(0) shouldBeJavascriptProject({ cwd, projectName, template: 'default' }) @@ -337,7 +367,9 @@ describe('create next app', () => { it('should use npm as the package manager on supplying --use-npm', async () => { await useTempDir(async (cwd) => { const projectName = 'use-npm' - const res = await run([projectName, '--js', '--use-npm'], { cwd }) + const res = await run([projectName, '--js', '--eslint', '--use-npm'], { + cwd, + }) expect(res.exitCode).toBe(0) shouldBeJavascriptProject({ cwd, projectName, template: 'default' }) @@ -351,6 +383,7 @@ describe('create next app', () => { [ projectName, '--js', + '--eslint', '--use-npm', '--example', `${exampleRepo}/${examplePath}`, @@ -376,7 +409,9 @@ describe('create next app', () => { it('should use pnpm as the package manager on supplying --use-pnpm', async () => { await useTempDir(async (cwd) => { const projectName = 'use-pnpm' - const res = await run([projectName, '--js', '--use-pnpm'], { cwd }) + const res = await run([projectName, '--js', '--eslint', '--use-pnpm'], { + cwd, + }) expect(res.exitCode).toBe(0) projectFilesShouldExist({ @@ -408,6 +443,7 @@ describe('create next app', () => { [ projectName, '--js', + '--eslint', '--use-pnpm', '--example', `${exampleRepo}/${examplePath}`, @@ -433,7 +469,7 @@ describe('create next app', () => { it('should infer npm as the package manager', async () => { await useTempDir(async (cwd) => { const projectName = 'infer-package-manager-npm' - const res = await run([projectName, '--js'], { + const res = await run([projectName, '--js', '--eslint'], { cwd, env: { ...process.env, npm_config_user_agent: 'npm' }, }) @@ -456,7 +492,13 @@ describe('create next app', () => { await useTempDir(async (cwd) => { const projectName = 'infer-package-manager-npm' const res = await run( - [projectName, '--js', '--example', `${exampleRepo}/${examplePath}`], + [ + projectName, + '--js', + '--eslint', + '--example', + `${exampleRepo}/${examplePath}`, + ], { cwd, env: { ...process.env, npm_config_user_agent: 'npm' } } ) @@ -483,7 +525,7 @@ describe('create next app', () => { await useTempDir(async (cwd) => { const projectName = 'infer-package-manager-yarn' - const res = await run([projectName, '--js'], { + const res = await run([projectName, '--js', '--eslint'], { cwd, env: { ...process.env, npm_config_user_agent: 'yarn' }, }) @@ -513,7 +555,13 @@ describe('create next app', () => { await useTempDir(async (cwd) => { const projectName = 'infer-package-manager-npm' const res = await run( - [projectName, '--js', '--example', `${exampleRepo}/${examplePath}`], + [ + projectName, + '--js', + '--eslint', + '--example', + `${exampleRepo}/${examplePath}`, + ], { cwd, env: { ...process.env, npm_config_user_agent: 'yarn' } } ) @@ -540,7 +588,7 @@ describe('create next app', () => { await useTempDir(async (cwd) => { const projectName = 'infer-package-manager' - const res = await run([projectName, '--js'], { + const res = await run([projectName, '--js', '--eslint'], { cwd, env: { ...process.env, npm_config_user_agent: 'pnpm' }, }) @@ -571,7 +619,13 @@ it('should infer pnpm as the package manager with example', async () => { await useTempDir(async (cwd) => { const projectName = 'infer-package-manager-npm' const res = await run( - [projectName, '--js', '--example', `${exampleRepo}/${examplePath}`], + [ + projectName, + '--js', + '--eslint', + '--example', + `${exampleRepo}/${examplePath}`, + ], { cwd, env: { ...process.env, npm_config_user_agent: 'pnpm' } } ) diff --git a/test/integration/create-next-app/lib/specification.ts b/test/integration/create-next-app/lib/specification.ts index 9b85225fad574..204e322f708a7 100644 --- a/test/integration/create-next-app/lib/specification.ts +++ b/test/integration/create-next-app/lib/specification.ts @@ -28,8 +28,8 @@ export const projectSpecification: ProjectSpecification = { 'node_modules/next', '.gitignore', ], - deps: ['next', 'react', 'react-dom'], - devDeps: ['eslint', 'eslint-config-next'], + deps: ['next', 'react', 'react-dom', 'eslint', 'eslint-config-next'], + devDeps: [], }, default: { js: { @@ -45,13 +45,8 @@ export const projectSpecification: ProjectSpecification = { 'tsconfig.json', 'next-env.d.ts', ], - deps: [], - devDeps: [ - '@types/node', - '@types/react', - '@types/react-dom', - 'typescript', - ], + deps: ['@types/node', '@types/react', '@types/react-dom', 'typescript'], + devDeps: [], }, }, app: { @@ -61,13 +56,8 @@ export const projectSpecification: ProjectSpecification = { files: ['app/page.jsx', 'app/layout.jsx', 'pages/api/hello.js'], }, ts: { - deps: [], - devDeps: [ - '@types/node', - '@types/react', - '@types/react-dom', - 'typescript', - ], + deps: ['@types/node', '@types/react', '@types/react-dom', 'typescript'], + devDeps: [], files: [ 'app/page.tsx', 'app/layout.tsx', diff --git a/test/integration/create-next-app/lib/utils.ts b/test/integration/create-next-app/lib/utils.ts index d0c3d1bf214f7..ca029e1984442 100644 --- a/test/integration/create-next-app/lib/utils.ts +++ b/test/integration/create-next-app/lib/utils.ts @@ -69,7 +69,7 @@ export const projectDepsShouldBe = ({ }: ProjectDeps) => { const projectRoot = resolve(cwd, projectName) const pkgJson = require(resolve(projectRoot, 'package.json')) - expect(Object.keys(pkgJson[type]).sort()).toEqual(deps.sort()) + expect(Object.keys(pkgJson[type] || {}).sort()).toEqual(deps.sort()) } export const shouldBeTemplateProject = ({ diff --git a/test/integration/create-next-app/templates.test.ts b/test/integration/create-next-app/templates.test.ts index ad1762668d23e..b47eec33febf1 100644 --- a/test/integration/create-next-app/templates.test.ts +++ b/test/integration/create-next-app/templates.test.ts @@ -25,7 +25,7 @@ describe('create-next-app templates', () => { /** * Start the create-next-app call. */ - const childProcess = createNextApp([projectName], { cwd }) + const childProcess = createNextApp([projectName, '--eslint'], { cwd }) /** * Wait for the prompt to display. */ @@ -54,7 +54,9 @@ describe('create-next-app templates', () => { it('should create TS projects with --ts, --typescript', async () => { await useTempDir(async (cwd) => { const projectName = 'typescript-test' - const childProcess = createNextApp([projectName, '--ts'], { cwd }) + const childProcess = createNextApp([projectName, '--ts', '--eslint'], { + cwd, + }) const exitCode = await spawnExitPromise(childProcess) expect(exitCode).toBe(0) @@ -65,7 +67,9 @@ describe('create-next-app templates', () => { it('should create JS projects with --js, --javascript', async () => { await useTempDir(async (cwd) => { const projectName = 'javascript-test' - const childProcess = createNextApp([projectName, '--js'], { cwd }) + const childProcess = createNextApp([projectName, '--js', '--eslint'], { + cwd, + }) const exitCode = await spawnExitPromise(childProcess) expect(exitCode).toBe(0) @@ -79,7 +83,7 @@ describe('create-next-app --experimental-app-dir', () => { await useTempDir(async (cwd) => { const projectName = 'appdir-test' const childProcess = createNextApp( - [projectName, '--ts', '--experimental-app'], + [projectName, '--ts', '--experimental-app', '--eslint'], { cwd, } @@ -95,7 +99,7 @@ describe('create-next-app --experimental-app-dir', () => { await useTempDir(async (cwd) => { const projectName = 'appdir-test' const childProcess = createNextApp( - [projectName, '--js', '--experimental-app'], + [projectName, '--js', '--experimental-app', '--eslint'], { cwd, } diff --git a/test/lib/use-temp-dir.ts b/test/lib/use-temp-dir.ts index ac5ee243c8e71..f53fcc6aebfc2 100644 --- a/test/lib/use-temp-dir.ts +++ b/test/lib/use-temp-dir.ts @@ -10,7 +10,10 @@ export async function useTempDir( fn: (folder: string) => void | Promise, mode?: string | number ) { - const folder = path.join(os.tmpdir(), Math.random().toString(36).slice(2)) + const folder = path.join( + os.tmpdir(), + 'next-test-' + Math.random().toString(36).slice(2) + ) await fs.mkdirp(folder) if (mode) {