diff --git a/packages/next/cli/next-lint.ts b/packages/next/cli/next-lint.ts index 02e4216008fb8..5c07725468732 100755 --- a/packages/next/cli/next-lint.ts +++ b/packages/next/cli/next-lint.ts @@ -5,23 +5,65 @@ 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' +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', + cacheStrategy: args['--cache-strategy'] || 'metadata', + errorOnUnmatchedPattern: !Boolean(args['--no-error-on-unmatched-pattern']), +}) + const nextLint: cliCommand = (argv) => { const validArgs: arg.Spec = { // Types '--help': Boolean, + '--base-dir': String, '--dir': [String], // 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, + '--no-inline-config': Boolean, + '--report-unused-disable-directives': String, + '--cache': Boolean, + '--cache-location': String, + '--cache-strategy': String, + '--no-error-on-unmatched-pattern': Boolean, + + // Aliases + '-c': '--config', + } + let args: arg.Result try { - args = arg(validArgs, { argv }) + args = arg({ ...validArgs, ...validEslintArgs }, { argv }) } catch (error) { if (error.code === 'ARG_UNKNOWN_OPTION') { return printAndExit(error.message, 1) @@ -37,14 +79,41 @@ const nextLint: cliCommand = (argv) => { Usage $ next lint [options] - + represents the directory of the Next.js application. If no directory is provided, the current directory will be used. Options - -h - list this help - -d - set directory, or directories, to run ESLint (defaults to only 'pages') - `, + 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 + + 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 + + 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 + --cache-strategy String Strategy to use for detecting changed files - either: metadata or content - default: metadata + + Miscellaneous: + --no-error-on-unmatched-pattern Prevent errors when pattern is unmatched - default: false + `, 0 ) } @@ -57,7 +126,7 @@ const nextLint: cliCommand = (argv) => { } const dirs: string[] = args['--dir'] - const lintDirs = (dirs ?? ['pages', 'components', 'lib']).reduce( + const lintDirs = (dirs ?? ESLINT_DEFAULT_DIRS).reduce( (res: string[], d: string) => { const currDir = join(baseDir, d) if (!existsSync(currDir)) return res @@ -67,7 +136,7 @@ const nextLint: cliCommand = (argv) => { [] ) - runLintCheck(baseDir, lintDirs) + runLintCheck(baseDir, lintDirs, false, eslintOptions(args)) .then((results) => { if (results) { console.log(results) diff --git a/packages/next/lib/constants.ts b/packages/next/lib/constants.ts index 28636f4594104..8c56f65d5ceca 100644 --- a/packages/next/lib/constants.ts +++ b/packages/next/lib/constants.ts @@ -48,3 +48,12 @@ export const GSSP_COMPONENT_MEMBER_ERROR = `can not be attached to a page's comp export const NON_STANDARD_NODE_ENV = `You are using a non-standard "NODE_ENV" value in your environment. This creates inconsistencies in the project and is strongly advised against. Read more: https://nextjs.org/docs/messages/non-standard-node-env` export const SSG_FALLBACK_EXPORT_ERROR = `Pages with \`fallback\` enabled in \`getStaticPaths\` can not be exported. See more info here: https://nextjs.org/docs/messages/ssg-fallback-true-export` + +export const ESLINT_DEFAULT_DIRS = [ + 'pages', + 'components', + 'lib', + 'src/pages', + 'src/components', + 'src/lib', +] diff --git a/packages/next/lib/eslint/runLintCheck.ts b/packages/next/lib/eslint/runLintCheck.ts index aed9f452af043..b176881efe220 100644 --- a/packages/next/lib/eslint/runLintCheck.ts +++ b/packages/next/lib/eslint/runLintCheck.ts @@ -1,5 +1,6 @@ import { promises as fs } from 'fs' import chalk from 'chalk' +import path from 'path' import findUp from 'next/dist/compiled/find-up' import semver from 'next/dist/compiled/semver' @@ -7,7 +8,7 @@ import * as CommentJson from 'next/dist/compiled/comment-json' import { formatResults } from './customFormatter' import { writeDefaultConfig } from './writeDefaultConfig' -import { findPagesDir } from '../find-pages-dir' +import { existsSync, findPagesDir } from '../find-pages-dir' import { CompileError } from '../compile-error' import { hasNecessaryDependencies, @@ -21,16 +22,13 @@ type Config = { rules: { [key: string]: Array } } -const linteableFiles = (dir: string) => { - return `${dir}/**/*.{${['jsx', 'js', 'ts', 'tsx'].join(',')}}` -} - async function lint( deps: NecessaryDependencies, baseDir: string, lintDirs: string[], eslintrcFile: string | null, - pkgJsonPath: string | null + pkgJsonPath: string | null, + eslintOptions: any = null ): Promise { // Load ESLint after we're sure it exists: const mod = await import(deps.resolved) @@ -41,23 +39,23 @@ async function lint( const eslintVersion: string | undefined = mod?.CLIEngine?.version if (!eslintVersion || semver.lt(eslintVersion, '7.0.0')) { - Log.error( - `Your project has an older version of ESLint installed${ - eslintVersion ? ' (' + eslintVersion + ')' : '' - }. Please upgrade to ESLint version 7 or later` - ) - return null + return `${chalk.red( + 'error' + )} - Your project has an older version of ESLint installed${ + eslintVersion ? ' (' + eslintVersion + ')' : '' + }. Please upgrade to ESLint version 7 or later` } - Log.error( - `ESLint class not found. Please upgrade to ESLint version 7 or later` - ) - return null + return `${chalk.red( + 'error' + )} - ESLint class not found. Please upgrade to ESLint version 7 or later` } let options: any = { useEslintrc: true, baseConfig: {}, + extensions: ['.js', '.jsx', '.ts', '.tsx'], + ...eslintOptions, } let eslint = new ESLint(options) @@ -101,18 +99,21 @@ async function lint( eslint = new ESLint(options) } } + const results = await eslint.lintFiles(lintDirs) + if (options.fix) await ESLint.outputFixes(results) - const results = await eslint.lintFiles(lintDirs.map(linteableFiles)) if (ESLint.getErrorResults(results)?.length > 0) { throw new CompileError(await formatResults(baseDir, results)) } + return results?.length > 0 ? formatResults(baseDir, results) : null } export async function runLintCheck( baseDir: string, lintDirs: string[], - lintDuringBuild: boolean = false + lintDuringBuild: boolean = false, + eslintOptions: any = null ): Promise { try { // Find user's .eslintrc file @@ -158,10 +159,23 @@ export async function runLintCheck( ) // Write default ESLint config if none is present - await writeDefaultConfig(eslintrcFile, pkgJsonPath, packageJsonConfig) + // Check for /pages and src/pages is to make sure this happens in Next.js folder + if ( + existsSync(path.join(baseDir, 'pages')) || + existsSync(path.join(baseDir, 'src/pages')) + ) { + await writeDefaultConfig(eslintrcFile, pkgJsonPath, packageJsonConfig) + } // Run ESLint - return await lint(deps, baseDir, lintDirs, eslintrcFile, pkgJsonPath) + return await lint( + deps, + baseDir, + lintDirs, + eslintrcFile, + pkgJsonPath, + eslintOptions + ) } catch (err) { throw err } diff --git a/packages/next/lib/verifyAndLint.ts b/packages/next/lib/verifyAndLint.ts index 798f2b6ff9360..3ee163b1b0f0b 100644 --- a/packages/next/lib/verifyAndLint.ts +++ b/packages/next/lib/verifyAndLint.ts @@ -2,6 +2,7 @@ import chalk from 'chalk' import { Worker } from 'jest-worker' import { existsSync } from 'fs' import { join } from 'path' +import { ESLINT_DEFAULT_DIRS } from './constants' export async function verifyAndLint( dir: string, @@ -20,7 +21,7 @@ export async function verifyAndLint( lintWorkers.getStdout().pipe(process.stdout) lintWorkers.getStderr().pipe(process.stderr) - const lintDirs = (configLintDirs ?? ['pages', 'components', 'lib']).reduce( + const lintDirs = (configLintDirs ?? ESLINT_DEFAULT_DIRS).reduce( (res: string[], d: string) => { const currDir = join(dir, d) if (!existsSync(currDir)) return res diff --git a/test/integration/eslint/first-time-setup/.eslintrc b/test/integration/eslint/first-time-setup/.eslintrc index 15b1ed91acf66..c3796c8047dd0 100644 --- a/test/integration/eslint/first-time-setup/.eslintrc +++ b/test/integration/eslint/first-time-setup/.eslintrc @@ -1,3 +1,4 @@ { + "root": true, "extends": "next" }