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

Add lint checking events #26089

Merged
merged 10 commits into from
Jun 15, 2021
3 changes: 2 additions & 1 deletion packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ export default async function build(
dir,
lintDirs,
config.experimental.cpus,
config.experimental.workerThreads
config.experimental.workerThreads,
telemetry
)
})
}
Expand Down
37 changes: 33 additions & 4 deletions packages/next/cli/next-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ 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 '../next-server/server/config'
import { PHASE_PRODUCTION_BUILD } from '../next-server/lib/constants'
import { eventLintCheckCompleted } from '../telemetry/events'
import { CompileError } from '../lib/compile-error'

const eslintOptions = (args: arg.Spec) => ({
overrideConfigFile: args['--config'] || null,
Expand Down Expand Up @@ -135,11 +140,35 @@ const nextLint: cliCommand = (argv) => {
},
[]
)

runLintCheck(baseDir, lintDirs, false, eslintOptions(args))
.then((results) => {
if (results) {
console.log(results)
.then(async (lintResults) => {
const lintOutput =
typeof lintResults === 'string' ? lintResults : lintResults?.output

if (typeof lintResults !== 'string' && lintResults?.eventInfo) {
const conf = await loadConfig(PHASE_PRODUCTION_BUILD, baseDir)
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 {
console.log(chalk.green('✔ No ESLint warnings or errors'))
}
Expand Down
60 changes: 49 additions & 11 deletions packages/next/lib/eslint/customFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface LintMessage {
column: number
}

interface LintResult {
export interface LintResult {
filePath: string
messages: LintMessage[]
errorCount: number
Expand All @@ -28,7 +28,11 @@ function formatMessage(
dir: string,
messages: LintMessage[],
filePath: string
): string | void {
): {
output: string
nextPluginErrorCount: number
nextPluginWarningCount: number
} {
let fileName = path.posix.normalize(
path.relative(dir, filePath).replace(/\\/g, '/')
)
Expand All @@ -38,6 +42,8 @@ function formatMessage(
}

let output = '\n' + chalk.cyan(fileName)
let nextPluginWarningCount = 0
let nextPluginErrorCount = 0

for (let i = 0; i < messages.length; i++) {
const { message, severity, line, column, ruleId } = messages[i]
Expand All @@ -53,6 +59,14 @@ function formatMessage(
' '
}

if (ruleId?.includes('@next/next')) {
if (severity === MessageSeverity.Warning) {
nextPluginWarningCount += 1
} else {
nextPluginErrorCount += 1
}
}

if (severity === MessageSeverity.Warning) {
output += chalk.yellow.bold('Warning') + ': '
} else {
Expand All @@ -66,19 +80,43 @@ function formatMessage(
}
}

return output
return {
output,
nextPluginErrorCount,
nextPluginWarningCount,
}
}

export function formatResults(baseDir: string, results: LintResult[]): string {
export function formatResults(
baseDir: string,
results: LintResult[]
): {
output: string
totalNextPluginErrorCount: number
totalNextPluginWarningCount: number
} {
let totalNextPluginErrorCount = 0
let totalNextPluginWarningCount = 0

const formattedResults = results
.filter(({ messages }) => messages?.length)
.map(({ messages, filePath }) => formatMessage(baseDir, messages, filePath))
.map(({ messages, filePath }) => {
const res = formatMessage(baseDir, messages, filePath)
totalNextPluginErrorCount += res.nextPluginErrorCount
totalNextPluginWarningCount += res.nextPluginWarningCount
return res.output
})
.join('\n')

return formattedResults.length > 0
? formattedResults +
`\n\n${chalk.bold(
'Need to disable some ESLint rules? Learn more here:'
)} https://nextjs.org/docs/basic-features/eslint#disabling-rules\n`
: ''
return {
output:
formattedResults.length > 0
? formattedResults +
`\n\n${chalk.bold(
'Need to disable some ESLint rules? Learn more here:'
)} https://nextjs.org/docs/basic-features/eslint#disabling-rules\n`
: '',
totalNextPluginErrorCount,
totalNextPluginWarningCount,
}
}
46 changes: 37 additions & 9 deletions packages/next/lib/eslint/runLintCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import * as CommentJson from 'next/dist/compiled/comment-json'
import { formatResults } from './customFormatter'
import { writeDefaultConfig } from './writeDefaultConfig'
import { existsSync, findPagesDir } from '../find-pages-dir'
import { CompileError } from '../compile-error'
import {
hasNecessaryDependencies,
NecessaryDependencies,
} from '../has-necessary-dependencies'

import * as Log from '../../build/output/log'
import { EventLintCheckCompleted } from '../../telemetry/events/build'

type Config = {
plugins: string[]
Expand All @@ -29,14 +29,23 @@ async function lint(
eslintrcFile: string | null,
pkgJsonPath: string | null,
eslintOptions: any = null
): Promise<string | null> {
): Promise<
| string
| null
| {
output: string | null
isError: boolean
eventInfo: EventLintCheckCompleted
}
> {
// Load ESLint after we're sure it exists:
const mod = await import(deps.resolved)
const mod = await import(deps.resolved.get('eslint')!)

const { ESLint } = mod
let eslintVersion = ESLint.version

if (!ESLint) {
const eslintVersion: string | undefined = mod?.CLIEngine?.version
eslintVersion = mod?.CLIEngine?.version

if (!eslintVersion || semver.lt(eslintVersion, '7.0.0')) {
return `${chalk.red(
Expand Down Expand Up @@ -99,22 +108,41 @@ async function lint(
eslint = new ESLint(options)
}
}
const lintStart = process.hrtime()

const results = await eslint.lintFiles(lintDirs)
if (options.fix) await ESLint.outputFixes(results)

if (ESLint.getErrorResults(results)?.length > 0) {
throw new CompileError(await formatResults(baseDir, results))
const formattedResult = formatResults(baseDir, results)
const lintEnd = process.hrtime(lintStart)

return {
output: formattedResult.output,
isError: ESLint.getErrorResults(results)?.length > 0,
eventInfo: {
durationInSeconds: lintEnd[0],
eslintVersion: eslintVersion,
lintedFilesCount: results.length,
lintFix: !!options.fix,
nextEslintPluginVersion: nextEslintPluginIsEnabled
? require(path.join(
path.dirname(deps.resolved.get('eslint-config-next')!),
'package.json'
)).version
: null,
nextEslintPluginErrorsCount: formattedResult.totalNextPluginErrorCount,
nextEslintPluginWarningsCount:
formattedResult.totalNextPluginWarningCount,
},
}

return results?.length > 0 ? formatResults(baseDir, results) : null
}

export async function runLintCheck(
baseDir: string,
lintDirs: string[],
lintDuringBuild: boolean = false,
eslintOptions: any = null
): Promise<string | null> {
): ReturnType<typeof lint> {
try {
// Find user's .eslintrc file
const eslintrcFile =
Expand Down
6 changes: 2 additions & 4 deletions packages/next/lib/has-necessary-dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const requiredLintPackages = [
]

export type NecessaryDependencies = {
resolved: string
resolved: Map<string, string>
}

export async function hasNecessaryDependencies(
Expand Down Expand Up @@ -46,9 +46,7 @@ export async function hasNecessaryDependencies(

if (missingPackages.length < 1) {
return {
resolved: checkESLintDeps
? resolutions.get('eslint')!
: resolutions.get('typescript')!,
resolved: resolutions,
}
}

Expand Down
29 changes: 25 additions & 4 deletions packages/next/lib/verifyAndLint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { Worker } from 'jest-worker'
import { existsSync } from 'fs'
import { join } from 'path'
import { ESLINT_DEFAULT_DIRS } from './constants'
import { Telemetry } from '../telemetry/storage'
import { eventLintCheckCompleted } from '../telemetry/events'
import { CompileError } from './compile-error'

export async function verifyAndLint(
dir: string,
configLintDirs: string[] | undefined,
numWorkers: number | undefined,
enableWorkerThreads: boolean | undefined
enableWorkerThreads: boolean | undefined,
telemetry: Telemetry
): Promise<void> {
try {
const lintWorkers = new Worker(require.resolve('./eslint/runLintCheck'), {
Expand All @@ -32,13 +36,30 @@ export async function verifyAndLint(
)

const lintResults = await lintWorkers.runLintCheck(dir, lintDirs, true)
if (lintResults) {
console.log(lintResults)
const lintOutput =
typeof lintResults === 'string' ? lintResults : lintResults?.output

if (typeof lintResults !== 'string' && lintResults?.eventInfo) {
telemetry.record(
eventLintCheckCompleted({
...lintResults.eventInfo,
buildLint: true,
})
)
}

if (typeof lintResults !== 'string' && lintResults?.isError && lintOutput) {
await telemetry.flush()
throw new CompileError(lintOutput)
}

if (lintOutput) {
console.log(lintOutput)
}

lintWorkers.end()
} catch (err) {
if (err.type === 'CompileError') {
if (err.type === 'CompileError' || err instanceof CompileError) {
console.error(chalk.red('\nFailed to compile.'))
console.error(err.message)
process.exit(1)
Expand Down
4 changes: 3 additions & 1 deletion packages/next/lib/verifyTypeScriptSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export async function verifyTypeScriptSetup(
)

// Load TypeScript after we're sure it exists:
const ts = (await import(deps.resolved)) as typeof import('typescript')
const ts = (await import(
deps.resolved.get('typescript')!
)) as typeof import('typescript')

if (semver.lt(ts.version, '4.3.2')) {
log.warn(
Expand Down
21 changes: 21 additions & 0 deletions packages/next/telemetry/events/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,27 @@ export function eventTypeCheckCompleted(
}
}

const EVENT_LINT_CHECK_COMPLETED = 'NEXT_LINT_CHECK_COMPLETED'
export type EventLintCheckCompleted = {
durationInSeconds: number
eslintVersion: string | null
lintedFilesCount?: number
lintFix?: boolean
buildLint?: boolean
nextEslintPluginVersion?: string | null
nextEslintPluginErrorsCount?: number
nextEslintPluginWarningsCount?: number
}

export function eventLintCheckCompleted(
event: EventLintCheckCompleted
): { eventName: string; payload: EventLintCheckCompleted } {
return {
eventName: EVENT_LINT_CHECK_COMPLETED,
payload: event,
}
}

const EVENT_BUILD_COMPLETED = 'NEXT_BUILD_COMPLETED'
type EventBuildCompleted = {
durationInSeconds: number
Expand Down
Loading