Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions code-pushup.preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 44 additions & 27 deletions packages/plugin-eslint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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'] }),
],
};
```
Expand Down Expand Up @@ -105,7 +119,7 @@ export default {
plugins: [
// ...
await eslintPlugin(
{ eslintrc: '.eslintrc.js', patterns: ['src/**/*.js'] },
{ eslintrc: 'eslint.config.js', patterns: ['src/**/*.js'] },
{
groups: [
{
Expand Down Expand Up @@ -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' } })],
};
```

Expand All @@ -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'],
},
},
}),
),
],
};
```
Expand All @@ -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',
},
},
}),
),
],
};
```
Expand All @@ -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',
},
}),
),
],
};
```
Expand Down
21 changes: 16 additions & 5 deletions packages/plugin-eslint/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof eslintTargetObjectSchema>;

/** Transforms string/array patterns into normalized object format. */
export const eslintTargetSchema = z
.union([patternsSchema, eslintTargetObjectSchema])
.transform(
Expand All @@ -32,10 +36,17 @@ export const eslintTargetSchema = z

export type ESLintTarget = z.infer<typeof eslintTargetSchema>;

/** 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<typeof eslintPluginConfigSchema>;

Expand Down
87 changes: 87 additions & 0 deletions packages/plugin-eslint/src/lib/config.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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 only eslintrc is provided', () => {
expect(eslintTargetSchema.parse({ eslintrc: 'eslint.config.js' })).toEqual({
eslintrc: 'eslint.config.js',
patterns: '.',
});
});

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: '.' }]);
});
});
25 changes: 18 additions & 7 deletions packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
} from '@code-pushup/test-utils';
import { eslintPlugin } from './eslint-plugin.js';

describe('eslintPlugin', () => {

Check failure on line 15 in packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
const thisDir = fileURLToPath(path.dirname(import.meta.url));

const fixturesDir = path.join(thisDir, '..', '..', 'mocks', 'fixtures');
Expand All @@ -20,7 +20,7 @@
let cwdSpy: MockInstance<[], string>;
let platformSpy: MockInstance<[], NodeJS.Platform>;

beforeAll(async () => {

Check failure on line 23 in packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'beforeAll'.
await cp(
path.join(fixturesDir, 'nx-monorepo'),
path.join(tmpDir, 'nx-monorepo'),
Expand All @@ -33,18 +33,18 @@
{ recursive: true },
);
await restoreNxIgnoredFiles(path.join(tmpDir, 'todos-app'));
cwdSpy = vi.spyOn(process, 'cwd');

Check failure on line 36 in packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'vi'.
// Linux produces extra quotation marks for globs
platformSpy = vi.spyOn(os, 'platform').mockReturnValue('linux');

Check failure on line 38 in packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'vi'.
});

afterAll(async () => {

Check failure on line 41 in packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'afterAll'.
cwdSpy.mockRestore();
platformSpy.mockRestore();
await teardownTestFolder(tmpDir);
});

it('should initialize ESLint plugin for React application', async () => {

Check failure on line 47 in packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'it'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
cwdSpy.mockReturnValue(path.join(tmpDir, 'todos-app'));

const plugin = await eslintPlugin({
Expand All @@ -52,12 +52,12 @@
patterns: ['src/**/*.js', 'src/**/*.jsx'],
});

expect(plugin).toMatchSnapshot({

Check failure on line 55 in packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
version: expect.any(String),

Check failure on line 56 in packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
});
});

it('should initialize ESLint plugin for Nx project', async () => {

Check failure on line 60 in packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2582: Cannot find name 'it'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.
cwdSpy.mockReturnValue(path.join(tmpDir, 'nx-monorepo'));
const plugin = await eslintPlugin({
eslintrc: './packages/nx-plugin/eslint.config.js',
Expand All @@ -65,7 +65,7 @@
});

// expect rule from extended base eslint.config.js
expect(plugin.audits).toStrictEqual(

Check failure on line 68 in packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts

View workflow job for this annotation

GitHub Actions / Standalone mode

<✓> TypeScript | Semantic errors

TS2304: Cannot find name 'expect'.
expect.arrayContaining([
expect.objectContaining<Audit>({
slug: expect.stringMatching(/^nx-enforce-module-boundaries/),
Expand Down Expand Up @@ -130,7 +130,7 @@
groups: [{ slug: 'type-safety', title: 'Type safety', rules: [] }],
},
),
).rejects.toThrow('Invalid input');
).rejects.toThrow(`Invalid ${ansis.bold('ESLintPluginOptions')}`);
await expect(
eslintPlugin(
{
Expand All @@ -141,14 +141,25 @@
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 () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-eslint/src/lib/eslint-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
* })
* ]
Expand All @@ -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<PluginConfig> {
const targets = validate(eslintPluginConfigSchema, config);
Expand Down
10 changes: 10 additions & 0 deletions packages/plugin-eslint/src/lib/eslint-plugin.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
});
});