Skip to content

Commit

Permalink
[ESLint] Adds --output-file flag (#36420)
Browse files Browse the repository at this point in the history
Add support for `--output-file` on ESLint cli, based on this discussion #26179, and this closed PR #35154. With this flag, it is possible to save the output in a file and use it for any purpose.

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [x] Make sure the linting passes by running `yarn lint`


Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
  • Loading branch information
pepoeverton and ijjk committed Aug 8, 2022
1 parent 4199da0 commit 59b144c
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 4 deletions.
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 @@ -224,8 +226,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 @@ -269,6 +273,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 @@ -312,7 +317,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
46 changes: 46 additions & 0 deletions packages/next/lib/eslint/writeOutputFile.ts
@@ -0,0 +1,46 @@
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)) {
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 (err) {
Log.error(`There was a problem writing the output file: ${filePath}`)
console.error(err)
}
}
}
104 changes: 104 additions & 0 deletions test/integration/eslint/test/index.test.js
Expand Up @@ -663,5 +663,109 @@ describe('ESLint', () => {
expect(output).not.toContain('pages/index.js')
expect(output).not.toContain('Synchronous scripts should not be used.')
})

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>` element. 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:
'Synchronous scripts should not be used. 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>` element. 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(
'Synchronous scripts should not be used. 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}`
)
})
})
})

0 comments on commit 59b144c

Please sign in to comment.