Add prompt for ESLint to CNA (#42218)

Following up https://github.com/vercel/next.js/pull/42012 this adds an
additional prompt for include ESLint config/dependencies or not. As
discussed, this also removes the slow down from doing separate
`dependencies` and `devDependencies` installs since this separation is
no longer required now that we have `output: 'standalone'` which ensures
only actual necessary dependency files are used for production builds.

<details>

<summary>Before</summary>


https://user-images.githubusercontent.com/22380829/198953290-116b422d-4359-4aa9-9d82-b3265fde7b3f.mp4


</details>

<details>

<summary>After</summary>




https://user-images.githubusercontent.com/22380829/198953347-20dbf897-92b3-45ea-a9d2-cfb61622251d.mp4



</details>

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
This commit is contained in:
JJ Kasper 2022-10-31 08:51:50 -07:00 committed by GitHub
parent 2280a2dd56
commit b7ea0a5abe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 70 deletions

View file

@ -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

View file

@ -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<void> {
let repoInfo: RepoInfo | undefined
@ -216,6 +218,7 @@ export async function createApp({
mode,
packageManager,
isOnline,
eslint,
})
}

View file

@ -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<void> {
/**
* 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<void> {
// 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<void> {
},
}
)
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<void> {
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<void> {
appPath: resolvedProjectPath,
packageManager,
typescript: program.typescript,
eslint: program.eslint,
experimentalApp: program.experimentalApp,
})
}

View file

@ -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'

View file

@ -17,4 +17,5 @@ export interface InstallTemplateArgs {
template: TemplateType
mode: TemplateMode
eslint: boolean
}

View file

@ -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' } }
)

View file

@ -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',

View file

@ -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 = ({

View file

@ -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,
}

View file

@ -10,7 +10,10 @@ export async function useTempDir(
fn: (folder: string) => void | Promise<void>,
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) {