Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ESLint] Enable caching by default #28349

Merged
merged 4 commits into from Aug 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/basic-features/eslint.md
Expand Up @@ -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`:
Expand Down
5 changes: 3 additions & 2 deletions packages/next/build/index.ts
Expand Up @@ -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
Expand Down
22 changes: 13 additions & 9 deletions packages/next/cli/next-lint.ts
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many users likely opt-in to caching today using next lint --cache. I kept --cache as a dummy flag so it doesn't cause any breaking changes.

'--no-cache': Boolean,
'--cache-location': String,
'--error-on-unmatched-pattern': Boolean,
'--format': String,
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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({
Expand Down
1 change: 1 addition & 0 deletions packages/next/lib/eslint/runLintCheck.ts
Expand Up @@ -116,6 +116,7 @@ async function lint(
baseConfig: {},
errorOnUnmatchedPattern: false,
extensions: ['.js', '.jsx', '.ts', '.tsx'],
cache: true,
...eslintOptions,
}

Expand Down
5 changes: 4 additions & 1 deletion packages/next/lib/verifyAndLint.ts
Expand Up @@ -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,
Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions test/integration/eslint/eslint-cache-custom-dir/.eslintrc
@@ -0,0 +1,4 @@
{
"extends": "next",
"root": true
}
@@ -0,0 +1,3 @@
module.exports = {
distDir: 'build',
}
@@ -0,0 +1,7 @@
const Home = () => (
<div>
<p>Home</p>
</div>
)

export default Home
4 changes: 4 additions & 0 deletions test/integration/eslint/eslint-cache/.eslintrc
@@ -0,0 +1,4 @@
{
"extends": "next",
"root": true
}
7 changes: 7 additions & 0 deletions test/integration/eslint/eslint-cache/pages/index.js
@@ -0,0 +1,7 @@
const Home = () => (
<div>
<p>Home</p>
</div>
)

export default Home
78 changes: 78 additions & 0 deletions test/integration/eslint/test/index.test.js
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -429,5 +460,52 @@ describe('ESLint', () => {
expect(stdout).toContain('<script src="https://example.com" />')
expect(stdout).toContain('2 warnings found')
})

test('eslint caching is enabled by default', async () => {
const cacheDir = join(dirEslintCache, '.next', 'cache')

await fs.remove(cacheDir)
await nextLint(dirEslintCache, [])

const files = await fs.readdir(join(cacheDir, 'eslint/'))
const cacheExists = files.some((f) => /\.cache/.test(f))

expect(cacheExists).toBe(true)
})

test('eslint caching is disabled with the --no-cache flag', async () => {
const cacheDir = join(dirEslintCache, '.next', 'cache')

await fs.remove(cacheDir)
await nextLint(dirEslintCache, ['--no-cache'])

expect(fs.existsSync(join(cacheDir, 'eslint/'))).toBe(false)
})

test('the default 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 nextLint(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)
})

test('the --cache-location flag allows the user to define a separate cache location', async () => {
const cacheFile = join(dirEslintCache, '.eslintcache')

await fs.remove(cacheFile)
await nextLint(dirEslintCache, ['--cache-location', cacheFile])

expect(fs.existsSync(cacheFile)).toBe(true)
})
})
})