diff --git a/code-pushup.preset.ts b/code-pushup.preset.ts index a16b1759c..5a23a8c31 100644 --- a/code-pushup.preset.ts +++ b/code-pushup.preset.ts @@ -45,10 +45,7 @@ export async function configureEslintPlugin( plugins: [ projectName ? await eslintPlugin( - { - eslintrc: `packages/${projectName}/eslint.config.js`, - patterns: ['.'], - }, + { eslintrc: `packages/${projectName}/eslint.config.js` }, { artifacts: { // We leverage Nx dependsOn to only run all lint targets before we run code-pushup diff --git a/packages/plugin-eslint/README.md b/packages/plugin-eslint/README.md index a1ace02ff..8a4ca9027 100644 --- a/packages/plugin-eslint/README.md +++ b/packages/plugin-eslint/README.md @@ -43,7 +43,7 @@ Detected ESLint rules are mapped to Code PushUp audits. Audit reports are calcul 4. Add this plugin to the `plugins` array in your Code PushUp CLI config file (e.g. `code-pushup.config.js`). - Pass in the path to your ESLint config file, along with glob patterns for which files you wish to target (relative to `process.cwd()`). + The simplest configuration uses default settings and lints the current directory: ```js import eslintPlugin from '@code-pushup/eslint-plugin'; @@ -52,7 +52,21 @@ Detected ESLint rules are mapped to Code PushUp audits. Audit reports are calcul // ... plugins: [ // ... - await eslintPlugin({ eslintrc: '.eslintrc.js', patterns: ['src/**/*.js'] }), + await eslintPlugin(), + ], + }; + ``` + + You can optionally specify a custom ESLint config file and/or glob patterns for target files (relative to `process.cwd()`): + + ```js + import eslintPlugin from '@code-pushup/eslint-plugin'; + + export default { + // ... + plugins: [ + // ... + await eslintPlugin({ eslintrc: './eslint.config.js', patterns: ['src/**/*.js'] }), ], }; ``` @@ -105,7 +119,7 @@ export default { plugins: [ // ... await eslintPlugin( - { eslintrc: '.eslintrc.js', patterns: ['src/**/*.js'] }, + { eslintrc: 'eslint.config.js', patterns: ['src/**/*.js'] }, { groups: [ { @@ -239,19 +253,13 @@ The artifacts feature supports loading ESLint JSON reports that follow the stand ### Basic artifact configuration -Specify the path(s) to your ESLint JSON report files: +Specify the path(s) to your ESLint JSON report files in the artifacts option. If you don't need custom lint patterns, pass an empty object as the first parameter: ```js import eslintPlugin from '@code-pushup/eslint-plugin'; export default { - plugins: [ - await eslintPlugin({ - artifacts: { - artifactsPaths: './eslint-report.json', - }, - }), - ], + plugins: [await eslintPlugin({}, { artifacts: { artifactsPaths: './eslint-report.json' } })], }; ``` @@ -262,11 +270,14 @@ Use glob patterns to aggregate results from multiple files: ```js export default { plugins: [ - await eslintPlugin({ - artifacts: { - artifactsPaths: ['packages/**/eslint-report.json', 'apps/**/.eslint/*.json'], + await eslintPlugin( + {}, + { + artifacts: { + artifactsPaths: ['packages/**/eslint-report.json', 'apps/**/.eslint/*.json'], + }, }, - }), + ), ], }; ``` @@ -278,12 +289,15 @@ If you need to generate the artifacts before loading them, use the `generateArti ```js export default { plugins: [ - await eslintPlugin({ - artifacts: { - generateArtifactsCommand: 'npm run lint:report', - artifactsPaths: './eslint-report.json', + await eslintPlugin( + {}, + { + artifacts: { + generateArtifactsCommand: 'npm run lint:report', + artifactsPaths: './eslint-report.json', + }, }, - }), + ), ], }; ``` @@ -293,15 +307,18 @@ You can also specify the command with arguments: ```js export default { plugins: [ - await eslintPlugin({ - artifacts: { - generateArtifactsCommand: { - command: 'eslint', - args: ['src/**/*.{js,ts}', '--format=json', '--output-file=eslint-report.json'], + await eslintPlugin( + {}, + { + artifacts: { + generateArtifactsCommand: { + command: 'eslint', + args: ['src/**/*.{js,ts}', '--format=json', '--output-file=eslint-report.json'], + }, + artifactsPaths: './eslint-report.json', }, - artifactsPaths: './eslint-report.json', }, - }), + ), ], }; ``` diff --git a/packages/plugin-eslint/src/lib/config.ts b/packages/plugin-eslint/src/lib/config.ts index a42d8f42a..5fbab71fc 100644 --- a/packages/plugin-eslint/src/lib/config.ts +++ b/packages/plugin-eslint/src/lib/config.ts @@ -14,12 +14,16 @@ const eslintrcSchema = z .string() .meta({ description: 'Path to ESLint config file' }); -const eslintTargetObjectSchema = z.object({ - eslintrc: eslintrcSchema.optional(), - patterns: patternsSchema, -}); +const eslintTargetObjectSchema = z + .object({ + eslintrc: eslintrcSchema.optional(), + patterns: patternsSchema.optional().default('.'), + }) + .meta({ title: 'ESLintTargetObject' }); + type ESLintTargetObject = z.infer; +/** Transforms string/array patterns into normalized object format. */ export const eslintTargetSchema = z .union([patternsSchema, eslintTargetObjectSchema]) .transform( @@ -32,10 +36,17 @@ export const eslintTargetSchema = z export type ESLintTarget = z.infer; +/** First parameter of {@link eslintPlugin}. Defaults to current directory. */ export const eslintPluginConfigSchema = z .union([eslintTargetSchema, z.array(eslintTargetSchema).min(1)]) + .optional() + .default({ patterns: '.' }) .transform(toArray) - .meta({ title: 'ESLintPluginConfig' }); + .meta({ + title: 'ESLintPluginConfig', + description: + 'Optional configuration, defaults to linting current directory', + }); export type ESLintPluginConfig = z.input; diff --git a/packages/plugin-eslint/src/lib/config.unit.test.ts b/packages/plugin-eslint/src/lib/config.unit.test.ts new file mode 100644 index 000000000..3d08289ee --- /dev/null +++ b/packages/plugin-eslint/src/lib/config.unit.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest'; +import { eslintPluginConfigSchema, eslintTargetSchema } from './config.js'; + +describe('eslintTargetSchema', () => { + it('should accept string patterns', () => { + expect(eslintTargetSchema.parse('src/**/*.ts')).toStrictEqual({ + patterns: 'src/**/*.ts', + }); + }); + + it('should accept array patterns', () => { + expect(eslintTargetSchema.parse(['src', 'lib'])).toStrictEqual({ + patterns: ['src', 'lib'], + }); + }); + + it('should accept object with patterns', () => { + expect( + eslintTargetSchema.parse({ patterns: ['src/**/*.ts'] }), + ).toStrictEqual({ + patterns: ['src/**/*.ts'], + }); + }); + + it('should accept object with eslintrc and patterns', () => { + expect( + eslintTargetSchema.parse({ + eslintrc: 'eslint.config.js', + patterns: ['src'], + }), + ).toStrictEqual({ + eslintrc: 'eslint.config.js', + patterns: ['src'], + }); + }); + + it('should use default patterns when empty object is provided', () => { + expect(eslintTargetSchema.parse({})).toStrictEqual({ + patterns: '.', + }); + }); +}); + +describe('eslintPluginConfigSchema', () => { + it('should use default patterns when undefined is provided', () => { + expect(eslintPluginConfigSchema.parse(undefined)).toStrictEqual([ + { patterns: '.' }, + ]); + }); + + it('should use default patterns when empty object is provided', () => { + expect(eslintPluginConfigSchema.parse({})).toStrictEqual([ + { patterns: '.' }, + ]); + }); + + it('should accept string patterns', () => { + expect(eslintPluginConfigSchema.parse('src')).toStrictEqual([ + { patterns: 'src' }, + ]); + }); + + it('should accept array of targets', () => { + expect( + eslintPluginConfigSchema.parse([ + { patterns: ['src'] }, + { eslintrc: 'custom.config.js', patterns: ['lib'] }, + ]), + ).toStrictEqual([ + { patterns: ['src'] }, + { eslintrc: 'custom.config.js', patterns: ['lib'] }, + ]); + }); + + it('should use default patterns for targets with only eslintrc', () => { + expect( + eslintPluginConfigSchema.parse({ eslintrc: 'eslint.config.js' }), + ).toStrictEqual([{ eslintrc: 'eslint.config.js', patterns: '.' }]); + }); +}); diff --git a/packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts b/packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts index 34ef4628f..b92d97030 100644 --- a/packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts +++ b/packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts @@ -130,7 +130,7 @@ describe('eslintPlugin', () => { groups: [{ slug: 'type-safety', title: 'Type safety', rules: [] }], }, ), - ).rejects.toThrow('Invalid input'); + ).rejects.toThrow(`Invalid ${ansis.bold('ESLintPluginOptions')}`); await expect( eslintPlugin( { @@ -141,14 +141,25 @@ describe('eslintPlugin', () => { groups: [{ slug: 'type-safety', title: 'Type safety', rules: {} }], }, ), - ).rejects.toThrow('Invalid input'); + ).rejects.toThrow(`Invalid ${ansis.bold('ESLintPluginOptions')}`); }); - it('should throw when invalid parameters provided', async () => { - await expect( - // @ts-expect-error simulating invalid non-TS config - eslintPlugin({ eslintrc: '.eslintrc.json' }), - ).rejects.toThrow(`Invalid ${ansis.bold('ESLintPluginConfig')}`); + it('should initialize ESLint plugin without config using default patterns', async () => { + cwdSpy.mockReturnValue(path.join(tmpDir, 'todos-app')); + + const plugin = await eslintPlugin(); + + expect(plugin.slug).toBe('eslint'); + expect(plugin.audits.length).toBeGreaterThan(0); + }); + + it('should initialize ESLint plugin with only eslintrc using default patterns', async () => { + cwdSpy.mockReturnValue(path.join(tmpDir, 'todos-app')); + + const plugin = await eslintPlugin({ eslintrc: 'eslint.config.js' }); + + expect(plugin.slug).toBe('eslint'); + expect(plugin.audits.length).toBeGreaterThan(0); }); it("should throw if eslintrc file doesn't exist", async () => { diff --git a/packages/plugin-eslint/src/lib/eslint-plugin.ts b/packages/plugin-eslint/src/lib/eslint-plugin.ts index 97756a8e7..35af8f14b 100644 --- a/packages/plugin-eslint/src/lib/eslint-plugin.ts +++ b/packages/plugin-eslint/src/lib/eslint-plugin.ts @@ -21,7 +21,7 @@ import { createRunnerFunction } from './runner/runner.js'; * plugins: [ * // ... other plugins ... * await eslintPlugin({ - * eslintrc: '.eslintrc.json', + * eslintrc: 'eslint.config.js', * patterns: ['src', 'test/*.spec.js'] * }) * ] @@ -32,7 +32,7 @@ import { createRunnerFunction } from './runner/runner.js'; * @returns Plugin configuration as a promise. */ export async function eslintPlugin( - config: ESLintPluginConfig, + config?: ESLintPluginConfig, options?: ESLintPluginOptions, ): Promise { const targets = validate(eslintPluginConfigSchema, config); diff --git a/packages/plugin-eslint/src/lib/eslint-plugin.unit.test.ts b/packages/plugin-eslint/src/lib/eslint-plugin.unit.test.ts index 9de34c9e2..18749513f 100644 --- a/packages/plugin-eslint/src/lib/eslint-plugin.unit.test.ts +++ b/packages/plugin-eslint/src/lib/eslint-plugin.unit.test.ts @@ -25,4 +25,14 @@ describe('eslintPlugin', () => { expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); expect(pluginConfig.scoreTargets).toStrictEqual(scoreTargets); }); + + it('should use default patterns when called without config', async () => { + const pluginConfig = await eslintPlugin(); + + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + expect(listAuditsAndGroupsSpy).toHaveBeenCalledWith( + [{ patterns: '.' }], + undefined, + ); + }); });