diff --git a/docs/basic-features/eslint.md b/docs/basic-features/eslint.md index 736bbed3c7b2dd4..6d3028a6a5f17ba 100644 --- a/docs/basic-features/eslint.md +++ b/docs/basic-features/eslint.md @@ -139,6 +139,14 @@ Similarly, the `--dir` flag can be used for `next lint`: next lint --dir pages --dir utils ``` +## Caching + +To improve performance, information of files processed by ESLint are cached by default. This is stored in `.next/cache` or in your defined [build directory](/docs/api-reference/next.config.js/setting-a-custom-build-directory). If you include any ESLint rules that depend on more than the contents of a single source file and need to disable the cache, use the `--no-cache` flag with `next lint`. + +```bash +next lint --no-cache +``` + ## Disabling Rules If you would like to modify or disable any rules provided by the supported plugins (`react`, `react-hooks`, `next`), you can directly change them using the `rules` property in your `.eslintrc`: diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index bc8657328fa3150..061b19351fe1a0e 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -217,14 +217,15 @@ export default async function build( } const ignoreESLint = Boolean(config.eslint?.ignoreDuringBuilds) - const lintDirs = config.eslint?.dirs + const eslintCacheDir = path.join(cacheDir, 'eslint/') if (!ignoreESLint && runLint) { await nextBuildSpan .traceChild('verify-and-lint') .traceAsyncFn(async () => { await verifyAndLint( dir, - lintDirs, + eslintCacheDir, + config.eslint?.dirs, config.experimental.cpus, config.experimental.workerThreads, telemetry diff --git a/packages/next/cli/next-lint.ts b/packages/next/cli/next-lint.ts index da80f1b614ffc11..6285dfdc6350efb 100755 --- a/packages/next/cli/next-lint.ts +++ b/packages/next/cli/next-lint.ts @@ -14,7 +14,7 @@ import { PHASE_PRODUCTION_BUILD } from '../shared/lib/constants' import { eventLintCheckCompleted } from '../telemetry/events' import { CompileError } from '../lib/compile-error' -const eslintOptions = (args: arg.Spec) => ({ +const eslintOptions = (args: arg.Spec, defaultCacheLocation: string) => ({ overrideConfigFile: args['--config'] || null, extensions: args['--ext'] ?? ['.js', '.jsx', '.ts', '.tsx'], resolvePluginsRelativeTo: args['--resolve-plugins-relative-to'] || null, @@ -26,8 +26,8 @@ const eslintOptions = (args: arg.Spec) => ({ allowInlineConfig: !Boolean(args['--no-inline-config']), reportUnusedDisableDirectives: args['--report-unused-disable-directives'] || null, - cache: args['--cache'] ?? false, - cacheLocation: args['--cache-location'] || '.eslintcache', + cache: !Boolean(args['--no-cache']), + cacheLocation: args['--cache-location'] || defaultCacheLocation, errorOnUnmatchedPattern: args['--error-on-unmatched-pattern'] ? Boolean(args['--error-on-unmatched-pattern']) : false, @@ -61,7 +61,8 @@ const nextLint: cliCommand = async (argv) => { '--max-warnings': Number, '--no-inline-config': Boolean, '--report-unused-disable-directives': String, - '--cache': Boolean, + '--cache': Boolean, // Although cache is enabled by default, this dummy flag still exists to not cause any breaking changes + '--no-cache': Boolean, '--cache-location': String, '--error-on-unmatched-pattern': Boolean, '--format': String, @@ -127,7 +128,7 @@ const nextLint: cliCommand = async (argv) => { --report-unused-disable-directives Adds reported errors for unused eslint-disable directives ("error" | "warn" | "off") Caching: - --cache Only check changed files - default: false + --no-cache Disable caching --cache-location path::String Path to the cache file or directory - default: .eslintcache Miscellaneous: @@ -144,9 +145,9 @@ const nextLint: cliCommand = async (argv) => { printAndExit(`> No such directory exists as the project root: ${baseDir}`) } - const conf = await loadConfig(PHASE_PRODUCTION_BUILD, baseDir) + const nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, baseDir) - const dirs: string[] = args['--dir'] ?? conf.eslint?.dirs + const dirs: string[] = args['--dir'] ?? nextConfig.eslint?.dirs const lintDirs = (dirs ?? ESLINT_DEFAULT_DIRS).reduce( (res: string[], d: string) => { const currDir = join(baseDir, d) @@ -162,11 +163,14 @@ const nextLint: cliCommand = async (argv) => { const formatter = args['--format'] || null const strict = Boolean(args['--strict']) + const distDir = join(baseDir, nextConfig.distDir) + const defaultCacheLocation = join(distDir, 'cache', 'eslint/') + runLintCheck( baseDir, lintDirs, false, - eslintOptions(args), + eslintOptions(args, defaultCacheLocation), reportErrorsOnly, maxWarnings, formatter, @@ -178,7 +182,7 @@ const nextLint: cliCommand = async (argv) => { if (typeof lintResults !== 'string' && lintResults?.eventInfo) { const telemetry = new Telemetry({ - distDir: join(baseDir, conf.distDir), + distDir, }) telemetry.record( eventLintCheckCompleted({ diff --git a/packages/next/lib/eslint/runLintCheck.ts b/packages/next/lib/eslint/runLintCheck.ts index 0768f36cc12c39a..957fc2b5502d460 100644 --- a/packages/next/lib/eslint/runLintCheck.ts +++ b/packages/next/lib/eslint/runLintCheck.ts @@ -116,6 +116,7 @@ async function lint( baseConfig: {}, errorOnUnmatchedPattern: false, extensions: ['.js', '.jsx', '.ts', '.tsx'], + cache: true, ...eslintOptions, } diff --git a/packages/next/lib/verifyAndLint.ts b/packages/next/lib/verifyAndLint.ts index 0a36bbe78b1a637..9596464fb731644 100644 --- a/packages/next/lib/verifyAndLint.ts +++ b/packages/next/lib/verifyAndLint.ts @@ -9,6 +9,7 @@ import { CompileError } from './compile-error' export async function verifyAndLint( dir: string, + cacheLocation: string, configLintDirs: string[] | undefined, numWorkers: number | undefined, enableWorkerThreads: boolean | undefined, @@ -35,7 +36,9 @@ export async function verifyAndLint( [] ) - const lintResults = await lintWorkers.runLintCheck(dir, lintDirs, true) + const lintResults = await lintWorkers.runLintCheck(dir, lintDirs, true, { + cacheLocation, + }) const lintOutput = typeof lintResults === 'string' ? lintResults : lintResults?.output diff --git a/test/integration/eslint/eslint-cache-custom-dir/.eslintrc b/test/integration/eslint/eslint-cache-custom-dir/.eslintrc new file mode 100644 index 000000000000000..abd5579b49c1503 --- /dev/null +++ b/test/integration/eslint/eslint-cache-custom-dir/.eslintrc @@ -0,0 +1,4 @@ +{ + "extends": "next", + "root": true +} diff --git a/test/integration/eslint/eslint-cache-custom-dir/next.config.js b/test/integration/eslint/eslint-cache-custom-dir/next.config.js new file mode 100644 index 000000000000000..817f3c48988efcb --- /dev/null +++ b/test/integration/eslint/eslint-cache-custom-dir/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + distDir: 'build', +} diff --git a/test/integration/eslint/eslint-cache-custom-dir/pages/index.js b/test/integration/eslint/eslint-cache-custom-dir/pages/index.js new file mode 100644 index 000000000000000..5a2ab41ccf1aaaa --- /dev/null +++ b/test/integration/eslint/eslint-cache-custom-dir/pages/index.js @@ -0,0 +1,7 @@ +const Home = () => ( +
+

Home

+
+) + +export default Home diff --git a/test/integration/eslint/eslint-cache/.eslintrc b/test/integration/eslint/eslint-cache/.eslintrc new file mode 100644 index 000000000000000..abd5579b49c1503 --- /dev/null +++ b/test/integration/eslint/eslint-cache/.eslintrc @@ -0,0 +1,4 @@ +{ + "extends": "next", + "root": true +} diff --git a/test/integration/eslint/eslint-cache/pages/index.js b/test/integration/eslint/eslint-cache/pages/index.js new file mode 100644 index 000000000000000..5a2ab41ccf1aaaa --- /dev/null +++ b/test/integration/eslint/eslint-cache/pages/index.js @@ -0,0 +1,7 @@ +const Home = () => ( +
+

Home

+
+) + +export default Home diff --git a/test/integration/eslint/test/index.test.js b/test/integration/eslint/test/index.test.js index 9c7f70802bf4cfc..4b89eb55ab45e83 100644 --- a/test/integration/eslint/test/index.test.js +++ b/test/integration/eslint/test/index.test.js @@ -29,6 +29,8 @@ const dirEmptyDirectory = join(__dirname, '../empty-directory') const dirEslintIgnore = join(__dirname, '../eslint-ignore') const dirNoEslintPlugin = join(__dirname, '../no-eslint-plugin') const dirNoConfig = join(__dirname, '../no-config') +const dirEslintCache = join(__dirname, '../eslint-cache') +const dirEslintCacheCustomDir = join(__dirname, '../eslint-cache-custom-dir') describe('ESLint', () => { describe('Next Build', () => { @@ -144,6 +146,35 @@ describe('ESLint', () => { 'The Next.js plugin was not detected in your ESLint configuration' ) }) + + test('eslint caching is enabled', async () => { + const cacheDir = join(dirEslintCache, '.next', 'cache') + + await fs.remove(cacheDir) + await nextBuild(dirEslintCache, []) + + const files = await fs.readdir(join(cacheDir, 'eslint/')) + const cacheExists = files.some((f) => /\.cache/.test(f)) + + expect(cacheExists).toBe(true) + }) + + test('eslint cache lives in the user defined build directory', async () => { + const oldCacheDir = join(dirEslintCacheCustomDir, '.next', 'cache') + const newCacheDir = join(dirEslintCacheCustomDir, 'build', 'cache') + + await fs.remove(oldCacheDir) + await fs.remove(newCacheDir) + + await nextBuild(dirEslintCacheCustomDir, []) + + expect(fs.existsSync(oldCacheDir)).toBe(false) + + const files = await fs.readdir(join(newCacheDir, 'eslint/')) + const cacheExists = files.some((f) => /\.cache/.test(f)) + + expect(cacheExists).toBe(true) + }) }) describe('Next Lint', () => { @@ -429,5 +460,52 @@ describe('ESLint', () => { expect(stdout).toContain('