diff --git a/.gitignore b/.gitignore index 810a60f..fe42caa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ lib/ node_modules # tests +.lintpilotcache/ coverage/ # logs diff --git a/jest-config/setup.ts b/jest-config/setup.ts index 38edbee..c5d16f6 100644 --- a/jest-config/setup.ts +++ b/jest-config/setup.ts @@ -32,8 +32,11 @@ expect.extend({ isCalledWithExpected = expected.every((arg, index) => this.equals(received.mock.calls[0][index], arg)) } + const printExpected = this.utils.printExpected(expected) + const printReceived = this.utils.printReceived(received.mock.calls[0]) + return { - message: () => `expected ${received.getMockName()} to have been called exactly once with "${expected}" but received "${received.mock.calls[0]}"`, + message: () => `expected ${received.getMockName()} to have been called exactly once with arguments. Expected:\n\n${printExpected}\n\nReceived:\n\n${printReceived}\n`, pass: isCalledOnce && isCalledWithExpected, } }, diff --git a/src/index.ts b/src/index.ts index 90c7979..a1ef394 100755 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { Command } from 'commander' import { Events, Linter } from '@Types' +import { clearCacheDirectory } from '@Utils/cache' import colourLog from '@Utils/colourLog' import { notifyResults } from '@Utils/notifier' import { clearTerminal } from '@Utils/terminal' @@ -22,7 +23,7 @@ program .addHelpText('beforeAll', '\n✈️ Lint Pilot ✈️\n') .showHelpAfterError('\n💡 Run `lint-pilot --help` for more information.\n') -const runLinter = async ({ filePattern, fix, linter, ignore }: RunLinter) => { +const runLinter = async ({ cache, filePattern, fix, linter, ignore }: RunLinter) => { const startTime = new Date().getTime() colourLog.info(`Running ${linter.toLowerCase()}...`) @@ -33,6 +34,7 @@ const runLinter = async ({ filePattern, fix, linter, ignore }: RunLinter) => { }) const report: LintReport = await linters[linter].lintFiles({ + cache, files, fix, }) @@ -42,24 +44,27 @@ const runLinter = async ({ filePattern, fix, linter, ignore }: RunLinter) => { return report } -const runLintPilot = ({ filePatterns, fix, title, watch }: RunLintPilot) => { +const runLintPilot = ({ cache, filePatterns, fix, title, watch }: RunLintPilot) => { + const commonArgs = { + cache, + fix, + ignore: filePatterns.ignorePatterns, + } + Promise.all([ runLinter({ + ...commonArgs, filePattern: filePatterns.includePatterns[Linter.ESLint], - fix, - ignore: filePatterns.ignorePatterns, linter: Linter.ESLint, }), runLinter({ + ...commonArgs, filePattern: filePatterns.includePatterns[Linter.Markdownlint], - fix, - ignore: filePatterns.ignorePatterns, linter: Linter.Markdownlint, }), runLinter({ + ...commonArgs, filePattern: filePatterns.includePatterns[Linter.Stylelint], - fix, - ignore: filePatterns.ignorePatterns, linter: Linter.Stylelint, }), ]).then((reports) => { @@ -88,16 +93,23 @@ program .option('--fix', 'automatically fix problems', false) .option('-w, --watch', 'watch for file changes and re-run the linters', false) + .option('--cache', 'cache linting results', false) + .option('--clearCache', 'clear the cache', false) + .option('--ignore-dirs ', 'Directories to ignore globally') .option('--ignore-patterns ', 'File patterns to ignore globally') .option('--eslint-include ', 'File patterns to include for ESLint') .option('--debug', 'output additional debug information including the list of files being linted', false) - .action(({ debug, emoji, eslintInclude, fix, ignoreDirs, ignorePatterns, title, watch }) => { + .action(({ cache, clearCache, debug, emoji, eslintInclude, fix, ignoreDirs, ignorePatterns, title, watch }) => { clearTerminal() colourLog.title(`${emoji} ${title} ${emoji}`) console.log() + if (clearCache) { + clearCacheDirectory() + } + global.debug = debug const filePatterns = getFilePatterns({ @@ -105,7 +117,7 @@ program ignoreDirs, ignorePatterns, }) - runLintPilot({ filePatterns, fix, title, watch }) + runLintPilot({ cache, filePatterns, fix, title, watch }) if (watch) { watchFiles({ @@ -117,7 +129,7 @@ program clearTerminal() colourLog.info(message) console.log() - runLintPilot({ filePatterns, fix, title, watch }) + runLintPilot({ cache, filePatterns, fix, title, watch }) }) } }) diff --git a/src/linters/__tests__/eslint.spec.ts b/src/linters/__tests__/eslint.spec.ts index 98e746d..3dfb386 100644 --- a/src/linters/__tests__/eslint.spec.ts +++ b/src/linters/__tests__/eslint.spec.ts @@ -32,12 +32,30 @@ describe('eslint', () => { lintFilesMock.mockImplementationOnce(() => []) await eslintLib.lintFiles({ + cache: false, files: testFiles, fix: false }) expect(ESLint).toHaveBeenCalledOnceWith({ cache: false, + cacheLocation: undefined, + fix: false, + }) + }) + + it('creates a new ESLint instance with cacheing enabled', async () => { + lintFilesMock.mockImplementationOnce(() => []) + + await eslintLib.lintFiles({ + cache: true, + files: testFiles, + fix: false + }) + + expect(ESLint).toHaveBeenCalledOnceWith({ + cache: true, + cacheLocation: expect.stringContaining('.lintpilotcache/.eslintcache'), fix: false, }) }) @@ -46,6 +64,7 @@ describe('eslint', () => { lintFilesMock.mockImplementationOnce(() => []) await eslintLib.lintFiles({ + cache: false, files: testFiles, fix: false }) @@ -64,6 +83,7 @@ describe('eslint', () => { try { await eslintLib.lintFiles({ + cache: false, files: testFiles, fix: false }) @@ -79,6 +99,7 @@ describe('eslint', () => { lintFilesMock.mockImplementationOnce(() => lintResults) expect(await eslintLib.lintFiles({ + cache: false, files: [], fix: false })).toStrictEqual({ @@ -99,6 +120,7 @@ describe('eslint', () => { lintFilesMock.mockImplementationOnce(() => noErrorLintResults) expect(await eslintLib.lintFiles({ + cache: false, files: testFiles, fix: false })).toStrictEqual({ @@ -204,6 +226,7 @@ describe('eslint', () => { lintFilesMock.mockImplementationOnce(() => lintResults) expect(await eslintLib.lintFiles({ + cache: false, files: testFiles, fix: false })).toStrictEqual({ @@ -263,6 +286,7 @@ describe('eslint', () => { lintFilesMock.mockImplementationOnce(() => noErrorLintResults) await eslintLib.lintFiles({ + cache: false, files: testFiles, fix: false }) @@ -278,6 +302,7 @@ describe('eslint', () => { lintFilesMock.mockImplementationOnce(() => noErrorLintResults) await eslintLib.lintFiles({ + cache: false, files: testFiles, fix: true }) diff --git a/src/linters/__tests__/stylelint.spec.ts b/src/linters/__tests__/stylelint.spec.ts index e3e4bfd..88ebd0a 100644 --- a/src/linters/__tests__/stylelint.spec.ts +++ b/src/linters/__tests__/stylelint.spec.ts @@ -32,19 +32,49 @@ describe('stylelint', () => { })) await stylelintLib.lintFiles({ + cache: false, files: testFiles, fix: false, }) - expect(stylelint.lint).toHaveBeenCalledOnceWith(expect.objectContaining({ + expect(stylelint.lint).toHaveBeenCalledOnceWith({ allowEmptyInput: true, + cache: false, + cacheLocation: undefined, + config: expect.anything(), files: testFiles, fix: false, quietDeprecationWarnings: true, reportDescriptionlessDisables: true, reportInvalidScopeDisables: true, reportNeedlessDisables: true, + }) + }) + + it('calls stylelint.lint with cacheing enabled', async () => { + lintFilesMock.mockImplementationOnce(() => ({ + results: [], + ruleMetadata: {}, })) + + await stylelintLib.lintFiles({ + cache: true, + files: testFiles, + fix: false, + }) + + expect(stylelint.lint).toHaveBeenCalledOnceWith({ + allowEmptyInput: true, + cache: true, + cacheLocation: expect.stringContaining('.lintpilotcache/.stylelintcache'), + config: expect.anything(), + files: testFiles, + fix: false, + quietDeprecationWarnings: true, + reportDescriptionlessDisables: true, + reportInvalidScopeDisables: true, + reportNeedlessDisables: true, + }) }) it('exists the process when stylelint throws an error', async () => { @@ -58,6 +88,7 @@ describe('stylelint', () => { try { await stylelintLib.lintFiles({ + cache: false, files: testFiles, fix: false, }) @@ -76,6 +107,7 @@ describe('stylelint', () => { })) expect(await stylelintLib.lintFiles({ + cache: false, files: [], fix: false, })).toStrictEqual({ @@ -99,6 +131,7 @@ describe('stylelint', () => { })) expect(await stylelintLib.lintFiles({ + cache: false, files: testFiles, fix: false, })).toStrictEqual({ @@ -203,6 +236,7 @@ describe('stylelint', () => { })) expect(await stylelintLib.lintFiles({ + cache: false, files: testFiles, fix: false, })).toStrictEqual({ @@ -260,6 +294,7 @@ describe('stylelint', () => { })) await stylelintLib.lintFiles({ + cache: false, files: testFiles, fix: false, }) @@ -277,6 +312,7 @@ describe('stylelint', () => { })) await stylelintLib.lintFiles({ + cache: false, files: testFiles, fix: true, }) diff --git a/src/linters/eslint.ts b/src/linters/eslint.ts index 8660cfe..9118c3f 100644 --- a/src/linters/eslint.ts +++ b/src/linters/eslint.ts @@ -1,15 +1,17 @@ import { ESLint } from 'eslint' import { Linter, RuleSeverity } from '@Types' +import { getCacheDirectory } from '@Utils/cache' import colourLog from '@Utils/colourLog' import { formatResult } from '@Utils/transform' import type { LintFiles, LintReport, ReportResults, ReportSummary } from '@Types' -const lintFiles = async ({ files, fix }: LintFiles): Promise => { +const lintFiles = async ({ cache, files, fix }: LintFiles): Promise => { try { const eslint = new ESLint({ - cache: false, + cache, + cacheLocation: cache ? getCacheDirectory('.eslintcache') : undefined, fix, }) diff --git a/src/linters/markdownlint/__tests__/index.spec.ts b/src/linters/markdownlint/__tests__/index.spec.ts index c916764..9dc78cd 100644 --- a/src/linters/markdownlint/__tests__/index.spec.ts +++ b/src/linters/markdownlint/__tests__/index.spec.ts @@ -28,6 +28,7 @@ describe('markdownlint', () => { jest.mocked(markdownlintAsync).mockResolvedValueOnce({}) await markdownlintLib.lintFiles({ + cache: false, files: testFiles, fix: false, }) @@ -40,6 +41,7 @@ describe('markdownlint', () => { jest.mocked(markdownlintAsync).mockResolvedValueOnce({}) await markdownlintLib.lintFiles({ + cache: false, files: testFiles, fix: false, }) @@ -61,6 +63,7 @@ describe('markdownlint', () => { try { await markdownlintLib.lintFiles({ + cache: false, files: testFiles, fix: false, }) @@ -78,6 +81,7 @@ describe('markdownlint', () => { jest.mocked(markdownlintAsync).mockResolvedValueOnce(lintResults) await markdownlintLib.lintFiles({ + cache: false, files: testFiles, fix: false, }) @@ -97,6 +101,7 @@ describe('markdownlint', () => { jest.mocked(markdownlintAsync).mockResolvedValueOnce(lintResults) await markdownlintLib.lintFiles({ + cache: false, files: testFiles, fix: true, }) @@ -121,6 +126,7 @@ describe('markdownlint', () => { .mockResolvedValueOnce(lintResultsWithoutError) await markdownlintLib.lintFiles({ + cache: false, files: testFiles, fix: true, }) diff --git a/src/linters/stylelint.ts b/src/linters/stylelint.ts index 8f7ed1f..0d85b3b 100644 --- a/src/linters/stylelint.ts +++ b/src/linters/stylelint.ts @@ -1,12 +1,13 @@ import stylelint from 'stylelint' import { Linter, RuleSeverity } from '@Types' +import { getCacheDirectory } from '@Utils/cache' import colourLog from '@Utils/colourLog' import { formatResult } from '@Utils/transform' import type { LintFiles, LintReport, ReportResults, ReportSummary } from '@Types' -const lintFiles = async ({ files, fix }: LintFiles): Promise => { +const lintFiles = async ({ cache, files, fix }: LintFiles): Promise => { try { // TODO: Stylelint config, extensible? const { @@ -14,7 +15,8 @@ const lintFiles = async ({ files, fix }: LintFiles): Promise => { ruleMetadata, } = await stylelint.lint({ allowEmptyInput: true, - cache: false, + cache, + cacheLocation: cache ? getCacheDirectory('.stylelintcache') : undefined, config: { rules: { 'declaration-block-no-duplicate-properties': true, diff --git a/src/types/index.ts b/src/types/index.ts index 86df52c..cf9f16d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -62,11 +62,13 @@ interface FilePatterns { } interface LintFiles { + cache: boolean files: Array fix: boolean } interface RunLinter { + cache: boolean filePattern: Array fix: boolean ignore: Array @@ -74,6 +76,7 @@ interface RunLinter { } interface RunLintPilot { + cache: boolean filePatterns: FilePatterns fix: boolean title: string diff --git a/src/utils/__tests__/cache.spec.ts b/src/utils/__tests__/cache.spec.ts new file mode 100644 index 0000000..df94ee5 --- /dev/null +++ b/src/utils/__tests__/cache.spec.ts @@ -0,0 +1,53 @@ +import fs from 'fs' +import path from 'path' + +import colourLog from '@Utils/colourLog' + +import { clearCacheDirectory, getCacheDirectory } from '../cache' + +jest.mock('fs') +jest.mock('@Utils/colourLog', () => ({ + info: jest.fn(), +})) + +describe('clearCacheDirectory', () => { + + const expectedCacheDirectory = `${process.cwd()}/.lintpilotcache` + + it('clears the cache directory if it exists', () => { + jest.mocked(fs.existsSync).mockReturnValueOnce(true) + + clearCacheDirectory() + + expect(fs.existsSync).toHaveBeenCalledWith(expectedCacheDirectory) + expect(fs.rmSync).toHaveBeenCalledWith(expectedCacheDirectory, { + force: true, + recursive: true, + }) + expect(colourLog.info).toHaveBeenCalledWith('Cache cleared.\n') + }) + + it('does not attempt to clear the cache if the directory does not exist', () => { + jest.mocked(fs.existsSync).mockReturnValueOnce(false) + + clearCacheDirectory() + + expect(fs.existsSync).toHaveBeenCalledWith(expectedCacheDirectory) + expect(fs.rmSync).not.toHaveBeenCalled() + expect(colourLog.info).toHaveBeenCalledWith('No cache to clear.\n') + }) + +}) + +describe('getCacheDirectory', () => { + + it('returns the correct cache directory path for a given file', () => { + jest.spyOn(path, 'resolve') + + const result = getCacheDirectory('.eslintcache') + + expect(path.resolve).toHaveBeenCalledWith(process.cwd(), '.lintpilotcache', '.eslintcache') + expect(result).toEqual(`${process.cwd()}/.lintpilotcache/.eslintcache`) + }) + +}) diff --git a/src/utils/cache.ts b/src/utils/cache.ts new file mode 100644 index 0000000..2babd70 --- /dev/null +++ b/src/utils/cache.ts @@ -0,0 +1,27 @@ +import fs from 'fs' +import path from 'path' + +import colourLog from './colourLog' + +const CACHE_FOLDER = '.lintpilotcache' + +const clearCacheDirectory = () => { + const cacheDirectory = path.resolve(process.cwd(), CACHE_FOLDER) + + if (fs.existsSync(cacheDirectory)) { + fs.rmSync(cacheDirectory, { + force: true, + recursive: true, + }) + colourLog.info('Cache cleared.\n') + } else { + colourLog.info('No cache to clear.\n') + } +} + +const getCacheDirectory = (file: string) => path.resolve(process.cwd(), CACHE_FOLDER, file) + +export { + clearCacheDirectory, + getCacheDirectory, +}