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] Adds --output-file flag #36420

Merged
5 changes: 5 additions & 0 deletions packages/next/cli/next-lint.ts
Expand Up @@ -71,10 +71,12 @@ const nextLint: cliCommand = async (argv) => {
'--cache-strategy': String,
'--error-on-unmatched-pattern': Boolean,
'--format': String,
'--output-file': String,

// Aliases
'-c': '--config',
'-f': '--format',
'-o': '--output-file',
}

let args: arg.Result<arg.Spec>
Expand Down Expand Up @@ -127,6 +129,7 @@ const nextLint: cliCommand = async (argv) => {
--max-warnings Int Number of warnings to trigger nonzero exit code - default: -1

Output:
-o, --output-file path::String Specify file to write report to
-f, --format String Use a specific output format - default: Next.js custom formatter

Inline configuration comments:
Expand Down Expand Up @@ -171,6 +174,7 @@ const nextLint: cliCommand = async (argv) => {
const maxWarnings = args['--max-warnings'] ?? -1
const formatter = args['--format'] || null
const strict = Boolean(args['--strict'])
const outputFile = args['--output-file'] || null

const distDir = join(baseDir, nextConfig.distDir)
const defaultCacheLocation = join(distDir, 'cache', 'eslint/')
Expand All @@ -183,6 +187,7 @@ const nextLint: cliCommand = async (argv) => {
reportErrorsOnly,
maxWarnings,
formatter,
outputFile,
strict
)
.then(async (lintResults) => {
Expand Down
4 changes: 3 additions & 1 deletion packages/next/lib/eslint/customFormatter.ts
Expand Up @@ -100,6 +100,7 @@ export function formatResults(
format: (r: LintResult[]) => string
): {
output: string
outputWithMessages: string
totalNextPluginErrorCount: number
totalNextPluginWarningCount: number
} {
Expand All @@ -124,7 +125,8 @@ export function formatResults(
.join('\n')

return {
output:
output: output,
outputWithMessages:
resultsWithMessages.length > 0
? output +
`\n\n${chalk.cyan(
Expand Down
12 changes: 9 additions & 3 deletions packages/next/lib/eslint/runLintCheck.ts
Expand Up @@ -9,6 +9,7 @@ import * as CommentJson from 'next/dist/compiled/comment-json'
import { LintResult, formatResults } from './customFormatter'
import { writeDefaultConfig } from './writeDefaultConfig'
import { hasEslintConfiguration } from './hasEslintConfiguration'
import { writeOutputFile } from './writeOutputFile'

import { ESLINT_PROMPT_VALUES } from '../constants'
import { existsSync, findPagesDir } from '../find-pages-dir'
Expand Down Expand Up @@ -86,7 +87,8 @@ async function lint(
eslintOptions: any = null,
reportErrorsOnly: boolean = false,
maxWarnings: number = -1,
formatter: string | null = null
formatter: string | null = null,
outputFile: string | null = null
): Promise<
| string
| null
Expand Down Expand Up @@ -221,8 +223,10 @@ async function lint(
0
)

if (outputFile) await writeOutputFile(outputFile, formattedResult.output)

return {
output: formattedResult.output,
output: formattedResult.outputWithMessages,
isError:
ESLint.getErrorResults(results)?.length > 0 ||
(maxWarnings >= 0 && totalWarnings > maxWarnings),
Expand Down Expand Up @@ -266,6 +270,7 @@ export async function runLintCheck(
reportErrorsOnly: boolean = false,
maxWarnings: number = -1,
formatter: string | null = null,
outputFile: string | null = null,
strict: boolean = false
): ReturnType<typeof lint> {
try {
Expand Down Expand Up @@ -309,7 +314,8 @@ export async function runLintCheck(
eslintOptions,
reportErrorsOnly,
maxWarnings,
formatter
formatter,
outputFile
)
} else {
// Display warning if no ESLint configuration is present during "next build"
Expand Down
45 changes: 45 additions & 0 deletions packages/next/lib/eslint/writeOutputFile.ts
@@ -0,0 +1,45 @@
import { promises as fs } from 'fs'
import path from 'path'
import * as Log from '../../build/output/log'
import isError from '../../lib/is-error'

/**
* Check if a given file path is a directory or not.
* @param {string} filePath The path to a file to check.
* @returns {Promise<boolean>} `true` if the path is a directory.
*/
async function isDirectory(filePath: string): Promise<boolean> {
try {
return (await fs.stat(filePath)).isDirectory()
} catch (error) {
if (
isError(error) &&
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
) {
return false
}
throw error
}
}
/**
* Create a file with eslint output data
* @param {string} outputFile The name file that needs to be created
* @param {string} outputData The data that needs to be inserted into the file
*/
export async function writeOutputFile(outputFile: string, outputData: string) {
const filePath = path.resolve(process.cwd(), outputFile)

if (await isDirectory(filePath)) {
pepoeverton marked this conversation as resolved.
Show resolved Hide resolved
Log.error(
`Cannot write to output file path, it is a directory: ${filePath}`
)
} else {
try {
await fs.mkdir(path.dirname(filePath), { recursive: true })
await fs.writeFile(filePath, outputData)
Log.info(`The output file has been created: ${filePath}`)
} catch {
Log.error(`There was a problem writing the output file: ${filePath}`)
}
ijjk marked this conversation as resolved.
Show resolved Hide resolved
}
}
103 changes: 103 additions & 0 deletions test/integration/eslint/test/index.test.js
Expand Up @@ -604,5 +604,108 @@ describe('ESLint', () => {
expect(output).not.toContain('pages/index.js')
expect(output).not.toContain('External synchronous scripts are forbidden')
})

test('output flag create a file respecting the chosen format', async () => {
const filePath = `${__dirname}/output/output.json`
const { stdout, stderr } = await nextLint(
dirFileLinting,
['--format', 'json', '--output-file', filePath],
{
stdout: true,
stderr: true,
}
)

const cliOutput = stdout + stderr
const fileOutput = await fs.readJSON(filePath)

expect(cliOutput).toContain(
`The output file has been created: ${filePath}`
)

if (fileOutput && fileOutput.length) {
fileOutput.forEach((file) => {
expect(file).toHaveProperty('filePath')
expect(file).toHaveProperty('messages')
expect(file).toHaveProperty('errorCount')
expect(file).toHaveProperty('warningCount')
expect(file).toHaveProperty('fixableErrorCount')
expect(file).toHaveProperty('fixableWarningCount')
expect(file).toHaveProperty('source')
expect(file).toHaveProperty('usedDeprecatedRules')
})

expect(fileOutput[0].messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
message:
'img elements must have an alt prop, either with meaningful text, or an empty string for decorative images.',
}),
expect.objectContaining({
message: `Do not use <img>. Use Image from 'next/image' instead. See: https://nextjs.org/docs/messages/no-img-element`,
}),
])
)

expect(fileOutput[1].messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
message:
'External synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts',
}),
])
)
}
})

test('output flag create a file respecting the chosen format', async () => {
const filePath = `${__dirname}/output/output.txt`
const { stdout, stderr } = await nextLint(
dirFileLinting,
['--format', 'compact', '--output-file', filePath],
{
stdout: true,
stderr: true,
}
)

const cliOutput = stdout + stderr
const fileOutput = fs.readFileSync(filePath, 'utf8')

expect(cliOutput).toContain(
`The output file has been created: ${filePath}`
)

expect(fileOutput).toContain('file-linting/pages/bar.js')
expect(fileOutput).toContain(
'img elements must have an alt prop, either with meaningful text, or an empty string for decorative images.'
)
expect(fileOutput).toContain(
`Do not use <img>. Use Image from 'next/image' instead. See: https://nextjs.org/docs/messages/no-img-element`
)

expect(fileOutput).toContain('file-linting/pages/index.js')
expect(fileOutput).toContain(
'External synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts'
)
})

test('show error message when the file path is a directory', async () => {
const filePath = `${__dirname}`
const { stdout, stderr } = await nextLint(
dirFileLinting,
['--format', 'compact', '--output-file', filePath],
{
stdout: true,
stderr: true,
}
)

const cliOutput = stdout + stderr

expect(cliOutput).toContain(
`Cannot write to output file path, it is a directory: ${filePath}`
)
})
})
pepoeverton marked this conversation as resolved.
Show resolved Hide resolved
})