7a1c9eb17e
This PR introduces an improved developer experience when `next lint` is run for the first time. ### Current behavior `eslint-config-next` is a required package that must be installed before proceeding with `next lint` or `next build`: ![image](https://user-images.githubusercontent.com/12476932/123468791-43088100-d5c0-11eb-9ad0-5beb80b6c968.png) Although this has helped many developers start using the new ESLint config, this has also resulted in a few issues: - Users are required to install the full config (`eslint-config-next`) even if they do not use it or use the Next.js plugin directly (`eslint-plugin-next`). - #26348 - There's some confusion on why `eslint-config-next` needs to be installed or how it should be used instead of `eslint-plugin-next`. - #26574 - #26475 - #26438 ### New behavior Instead of enforcing `eslint-config-next` as a required package, this PR prompts the user by asking what config they would like to start. This happens when `next lint` is run for the first time **and** if no ESLint configuration is detected in the application. <img src="https://user-images.githubusercontent.com/12476932/124331177-e1668a80-db5c-11eb-8915-38d3dc20f5d4.gif" width="800" /> - The CLI will take care of installing `eslint` or `eslint-config-next` if either is not already installed - Users now have the option to choose between a strict configuration (`next/core-web-vitals`) or just the base configuration (`next`) - For users that decide to create their own ESLint configuration, or already have an existing one, **installing `eslint-config-next` will not be a requirement for `next lint` or `next build` to run**. A warning message will just show if the Next.js ESLint plugin is not detected in an ESLint config. <img width="682" alt="Screen Shot 2021-06-25 at 3 02 12 PM" src="https://user-images.githubusercontent.com/12476932/123473329-6cc4a680-d5c6-11eb-9a57-d5c0b89a2732.png"> --- In addition, this PR also: - Fixes #26348 - Updates documentation to make it more clear what approach to take for new and existing ESLint configurations
211 lines
6.9 KiB
JavaScript
Executable file
211 lines
6.9 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
import { existsSync } from 'fs'
|
|
import arg from 'next/dist/compiled/arg/index.js'
|
|
import { resolve, join } from 'path'
|
|
import chalk from 'chalk'
|
|
|
|
import { cliCommand } from '../bin/next'
|
|
import { ESLINT_DEFAULT_DIRS } from '../lib/constants'
|
|
import { runLintCheck } from '../lib/eslint/runLintCheck'
|
|
import { printAndExit } from '../server/lib/utils'
|
|
import { Telemetry } from '../telemetry/storage'
|
|
import loadConfig from '../server/config'
|
|
import { PHASE_PRODUCTION_BUILD } from '../shared/lib/constants'
|
|
import { eventLintCheckCompleted } from '../telemetry/events'
|
|
import { CompileError } from '../lib/compile-error'
|
|
|
|
const eslintOptions = (args: arg.Spec) => ({
|
|
overrideConfigFile: args['--config'] || null,
|
|
extensions: args['--ext'] ?? ['.js', '.jsx', '.ts', '.tsx'],
|
|
resolvePluginsRelativeTo: args['--resolve-plugins-relative-to'] || null,
|
|
rulePaths: args['--rulesdir'] ?? [],
|
|
fix: args['--fix'] ?? false,
|
|
fixTypes: args['--fix-type'] ?? null,
|
|
ignorePath: args['--ignore-path'] || null,
|
|
ignore: !Boolean(args['--no-ignore']),
|
|
allowInlineConfig: !Boolean(args['--no-inline-config']),
|
|
reportUnusedDisableDirectives:
|
|
args['--report-unused-disable-directives'] || null,
|
|
cache: args['--cache'] ?? false,
|
|
cacheLocation: args['--cache-location'] || '.eslintcache',
|
|
errorOnUnmatchedPattern: args['--error-on-unmatched-pattern']
|
|
? Boolean(args['--error-on-unmatched-pattern'])
|
|
: false,
|
|
})
|
|
|
|
const nextLint: cliCommand = async (argv) => {
|
|
const validArgs: arg.Spec = {
|
|
// Types
|
|
'--help': Boolean,
|
|
'--base-dir': String,
|
|
'--dir': [String],
|
|
'--strict': Boolean,
|
|
|
|
// Aliases
|
|
'-h': '--help',
|
|
'-b': '--base-dir',
|
|
'-d': '--dir',
|
|
}
|
|
|
|
const validEslintArgs: arg.Spec = {
|
|
// Types
|
|
'--config': String,
|
|
'--ext': [String],
|
|
'--resolve-plugins-relative-to': String,
|
|
'--rulesdir': [String],
|
|
'--fix': Boolean,
|
|
'--fix-type': [String],
|
|
'--ignore-path': String,
|
|
'--no-ignore': Boolean,
|
|
'--quiet': Boolean,
|
|
'--max-warnings': Number,
|
|
'--no-inline-config': Boolean,
|
|
'--report-unused-disable-directives': String,
|
|
'--cache': Boolean,
|
|
'--cache-location': String,
|
|
'--error-on-unmatched-pattern': Boolean,
|
|
'--format': String,
|
|
|
|
// Aliases
|
|
'-c': '--config',
|
|
'-f': '--format',
|
|
}
|
|
|
|
let args: arg.Result<arg.Spec>
|
|
try {
|
|
args = arg({ ...validArgs, ...validEslintArgs }, { argv })
|
|
} catch (error) {
|
|
if (error.code === 'ARG_UNKNOWN_OPTION') {
|
|
return printAndExit(error.message, 1)
|
|
}
|
|
throw error
|
|
}
|
|
if (args['--help']) {
|
|
printAndExit(
|
|
`
|
|
Description
|
|
Run ESLint on every file in specified directories.
|
|
If not configured, ESLint will be set up for the first time.
|
|
|
|
Usage
|
|
$ next lint <baseDir> [options]
|
|
|
|
<baseDir> represents the directory of the Next.js application.
|
|
If no directory is provided, the current directory will be used.
|
|
|
|
Options
|
|
Basic configuration:
|
|
-h, --help List this help
|
|
-d, --dir Array Set directory, or directories, to run ESLint - default: 'pages', 'components', and 'lib'
|
|
-c, --config path::String Use this configuration file, overriding all other config options
|
|
--ext [String] Specify JavaScript file extensions - default: .js, .jsx, .ts, .tsx
|
|
--resolve-plugins-relative-to path::String A folder where plugins should be resolved from, CWD by default
|
|
|
|
Initial setup:
|
|
--strict Creates an .eslintrc.json file using the Next.js strict configuration (only possible if no .eslintrc.json file is present)
|
|
|
|
Specifying rules:
|
|
--rulesdir [path::String] Use additional rules from this directory
|
|
|
|
Fixing problems:
|
|
--fix Automatically fix problems
|
|
--fix-type Array Specify the types of fixes to apply (problem, suggestion, layout)
|
|
|
|
Ignoring files:
|
|
--ignore-path path::String Specify path of ignore file
|
|
--no-ignore Disable use of ignore files and patterns
|
|
|
|
Handling warnings:
|
|
--quiet Report errors only - default: false
|
|
--max-warnings Int Number of warnings to trigger nonzero exit code - default: -1
|
|
|
|
Output:
|
|
-f, --format String Use a specific output format - default: Next.js custom formatter
|
|
|
|
Inline configuration comments:
|
|
--no-inline-config Prevent comments from changing config or rules
|
|
--report-unused-disable-directives Adds reported errors for unused eslint-disable directives ("error" | "warn" | "off")
|
|
|
|
Caching:
|
|
--cache Only check changed files - default: false
|
|
--cache-location path::String Path to the cache file or directory - default: .eslintcache
|
|
|
|
Miscellaneous:
|
|
--error-on-unmatched-pattern Show errors when any file patterns are unmatched - default: false
|
|
`,
|
|
0
|
|
)
|
|
}
|
|
|
|
const baseDir = resolve(args._[0] || '.')
|
|
|
|
// Check if the provided directory exists
|
|
if (!existsSync(baseDir)) {
|
|
printAndExit(`> No such directory exists as the project root: ${baseDir}`)
|
|
}
|
|
|
|
const conf = await loadConfig(PHASE_PRODUCTION_BUILD, baseDir)
|
|
|
|
const dirs: string[] = args['--dir'] ?? conf.eslint?.dirs
|
|
const lintDirs = (dirs ?? ESLINT_DEFAULT_DIRS).reduce(
|
|
(res: string[], d: string) => {
|
|
const currDir = join(baseDir, d)
|
|
if (!existsSync(currDir)) return res
|
|
res.push(currDir)
|
|
return res
|
|
},
|
|
[]
|
|
)
|
|
|
|
const reportErrorsOnly = Boolean(args['--quiet'])
|
|
const maxWarnings = args['--max-warnings'] ?? -1
|
|
const formatter = args['--format'] || null
|
|
const strict = Boolean(args['--strict'])
|
|
|
|
runLintCheck(
|
|
baseDir,
|
|
lintDirs,
|
|
false,
|
|
eslintOptions(args),
|
|
reportErrorsOnly,
|
|
maxWarnings,
|
|
formatter,
|
|
strict
|
|
)
|
|
.then(async (lintResults) => {
|
|
const lintOutput =
|
|
typeof lintResults === 'string' ? lintResults : lintResults?.output
|
|
|
|
if (typeof lintResults !== 'string' && lintResults?.eventInfo) {
|
|
const telemetry = new Telemetry({
|
|
distDir: join(baseDir, conf.distDir),
|
|
})
|
|
telemetry.record(
|
|
eventLintCheckCompleted({
|
|
...lintResults.eventInfo,
|
|
buildLint: false,
|
|
})
|
|
)
|
|
await telemetry.flush()
|
|
}
|
|
|
|
if (
|
|
typeof lintResults !== 'string' &&
|
|
lintResults?.isError &&
|
|
lintOutput
|
|
) {
|
|
throw new CompileError(lintOutput)
|
|
}
|
|
|
|
if (lintOutput) {
|
|
console.log(lintOutput)
|
|
} else if (lintResults && !lintOutput) {
|
|
console.log(chalk.green('✔ No ESLint warnings or errors'))
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
printAndExit(err.message)
|
|
})
|
|
}
|
|
|
|
export { nextLint }
|