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

feat(plugin-eslint): add support for flat config and v9 #880

Merged
merged 16 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
7e1ce39
feat(plugin-eslint): drop inline object support for eslintrc (incompa…
matejchalk Nov 20, 2024
9d00578
refactor(plugin-eslint): prepare different versions of loading rules
matejchalk Nov 20, 2024
04ef1c3
feat(plugin-eslint): implement rules loader for flat config
matejchalk Nov 20, 2024
7782cf6
feat(plugin-eslint): detect version of config format
matejchalk Nov 20, 2024
e61cee0
fix(plugin-eslint): remove unsupported parameter for ESLint 9+
matejchalk Nov 21, 2024
53864d0
feat(utils): implement and test helper function to find nearest file
matejchalk Nov 21, 2024
393cb5e
feat(plugin-eslint): search for flat config files in parent directories
matejchalk Nov 21, 2024
54c468d
test(plugin-eslint): unit test version detection
matejchalk Nov 21, 2024
bf9a81a
test(plugin-eslint): unit test rule parsing helpers
matejchalk Nov 21, 2024
85f4691
test(plugin-eslint): integration test loading rules from flat config
matejchalk Nov 21, 2024
02d4c8d
feat(plugin-eslint): move eslint to peer deps, add v9 to supported range
matejchalk Nov 22, 2024
52289ad
test(plugin-eslint): rewrite e2e test for flat config
matejchalk Nov 22, 2024
1373f77
fix(plugin-eslint): use LegacyESLint if ESLINT_USE_FLAT_CONFIG=false …
matejchalk Nov 22, 2024
face9c1
test(plugin-eslint-e2e): add e2e test for legacy config
matejchalk Nov 22, 2024
6293a9b
fix(plugin-eslint): ensure file url scheme needed for dynamic imports…
matejchalk Nov 22, 2024
3308f4b
test(plugin-eslint): increase timeout for windows
matejchalk Nov 22, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import eslintPlugin from '@code-pushup/eslint-plugin';

export default {
plugins: [await eslintPlugin({ patterns: ['src/*.js'] })],
};
13 changes: 13 additions & 0 deletions e2e/plugin-eslint-e2e/mocks/fixtures/flat-config/eslint.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** @type {import('eslint').Linter.FlatConfig[]} */
module.exports = [
{
ignores: ['code-pushup.config.ts'],
},
{
rules: {
eqeqeq: 'error',
'max-lines': ['warn', 100],
'no-unused-vars': 'warn',
},
},
];
9 changes: 9 additions & 0 deletions e2e/plugin-eslint-e2e/mocks/fixtures/flat-config/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function unusedFn() {
return '42';
}

module.exports = function orwell() {
if (2 + 2 == 5) {
console.log(1984);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import eslintPlugin from '@code-pushup/eslint-plugin';

export default {
plugins: [
await eslintPlugin({
eslintrc: '.eslintrc.json',
patterns: ['src/*.js'],
}),
],
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
function unusedFn() {
return '42';
}

module.exports = function consoleLog() {
console.log('No console.log()!');
};

This file was deleted.

134 changes: 111 additions & 23 deletions e2e/plugin-eslint-e2e/tests/__snapshots__/collect.e2e.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,35 +1,123 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`collect report with eslint-plugin NPM package > should run ESLint plugin and create report.json 1`] = `
exports[`collect report with eslint-plugin NPM package > should run ESLint plugin for flat config and create report.json 1`] = `
{
"categories": [
"packageName": "@code-pushup/core",
"plugins": [
{
"description": "Lint rules that find **potential bugs** in your code.",
"refs": [
"audits": [
{
"plugin": "eslint",
"slug": "problems",
"type": "group",
"weight": 1,
"description": "ESLint rule **eqeqeq**.",
"details": {
"issues": [
{
"message": "Expected '===' and instead saw '=='.",
"severity": "error",
"source": {
"file": "tmp/e2e/plugin-eslint-e2e/flat-config/src/index.js",
"position": {
"endColumn": 15,
"endLine": 6,
"startColumn": 13,
"startLine": 6,
},
},
},
],
},
"displayValue": "1 error",
"docsUrl": "https://eslint.org/docs/latest/rules/eqeqeq",
"score": 0,
"slug": "eqeqeq",
"title": "Require the use of \`===\` and \`!==\`",
"value": 1,
},
{
"description": "ESLint rule **max-lines**.

Custom options:

\`\`\`json
100
\`\`\`",
"details": {
"issues": [],
},
"displayValue": "passed",
"docsUrl": "https://eslint.org/docs/latest/rules/max-lines",
"score": 1,
"slug": "max-lines-71b54366cb01f77b",
"title": "Enforce a maximum number of lines per file",
"value": 0,
},
{
"description": "ESLint rule **no-unused-vars**.",
"details": {
"issues": [
{
"message": "'unusedFn' is defined but never used.",
"severity": "warning",
"source": {
"file": "tmp/e2e/plugin-eslint-e2e/flat-config/src/index.js",
"position": {
"endColumn": 18,
"endLine": 1,
"startColumn": 10,
"startLine": 1,
},
},
},
],
},
"displayValue": "1 warning",
"docsUrl": "https://eslint.org/docs/latest/rules/no-unused-vars",
"score": 0,
"slug": "no-unused-vars",
"title": "Disallow unused variables",
"value": 1,
},
],
"slug": "bug-prevention",
"title": "Bug prevention",
},
{
"description": "Lint rules that promote **good practices** and consistency in your code.",
"refs": [
"description": "Official Code PushUp ESLint plugin",
"docsUrl": "https://www.npmjs.com/package/@code-pushup/eslint-plugin",
"groups": [
{
"plugin": "eslint",
"description": "Code that either will cause an error or may cause confusing behavior. Developers should consider this a high priority to resolve.",
"refs": [
{
"slug": "no-unused-vars",
"weight": 1,
},
],
"slug": "problems",
"title": "Problems",
},
{
"description": "Something that could be done in a better way but no errors will occur if the code isn't changed.",
"refs": [
{
"slug": "eqeqeq",
"weight": 1,
},
{
"slug": "max-lines-71b54366cb01f77b",
"weight": 1,
},
],
"slug": "suggestions",
"type": "group",
"weight": 1,
"title": "Suggestions",
},
],
"slug": "code-style",
"title": "Code style",
"icon": "eslint",
"packageName": "@code-pushup/eslint-plugin",
"slug": "eslint",
"title": "ESLint",
},
],
}
`;

exports[`collect report with eslint-plugin NPM package > should run ESLint plugin for legacy config and create report.json 1`] = `
{
"packageName": "@code-pushup/core",
"plugins": [
{
Expand All @@ -42,7 +130,7 @@ exports[`collect report with eslint-plugin NPM package > should run ESLint plugi
"message": "'unusedFn' is defined but never used.",
"severity": "error",
"source": {
"file": "tmp/e2e/plugin-eslint-e2e/old-version/src/index.js",
"file": "tmp/e2e/plugin-eslint-e2e/legacy-config/src/index.js",
"position": {
"endColumn": 18,
"endLine": 1,
Expand All @@ -68,12 +156,12 @@ exports[`collect report with eslint-plugin NPM package > should run ESLint plugi
"message": "Unexpected console statement.",
"severity": "warning",
"source": {
"file": "tmp/e2e/plugin-eslint-e2e/old-version/src/index.js",
"file": "tmp/e2e/plugin-eslint-e2e/legacy-config/src/index.js",
"position": {
"endColumn": 14,
"endLine": 6,
"endLine": 5,
"startColumn": 3,
"startLine": 6,
"startLine": 5,
},
},
},
Expand Down
51 changes: 36 additions & 15 deletions e2e/plugin-eslint-e2e/tests/collect.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,61 @@ import { omitVariableReportData } from '@code-pushup/test-utils';
import { executeProcess, readJsonFile } from '@code-pushup/utils';

describe('collect report with eslint-plugin NPM package', () => {
const fixturesOldVersionDir = join(
'e2e',
'plugin-eslint-e2e',
'mocks',
'fixtures',
'old-version',
);
const fixturesDir = join('e2e', 'plugin-eslint-e2e', 'mocks', 'fixtures');
const fixturesFlatConfigDir = join(fixturesDir, 'flat-config');
const fixturesLegacyConfigDir = join(fixturesDir, 'legacy-config');

const envRoot = join('tmp', 'e2e', 'plugin-eslint-e2e');
const oldVersionDir = join(envRoot, 'old-version');
const oldVersionOutputDir = join(oldVersionDir, '.code-pushup');
const flatConfigDir = join(envRoot, 'flat-config');
const legacyConfigDir = join(envRoot, 'legacy-config');
const flatConfigOutputDir = join(flatConfigDir, '.code-pushup');
const legacyConfigOutputDir = join(legacyConfigDir, '.code-pushup');

beforeAll(async () => {
await cp(fixturesOldVersionDir, oldVersionDir, { recursive: true });
await cp(fixturesFlatConfigDir, flatConfigDir, { recursive: true });
await cp(fixturesLegacyConfigDir, legacyConfigDir, { recursive: true });
});

afterAll(async () => {
await teardownTestFolder(oldVersionDir);
await teardownTestFolder(flatConfigDir);
await teardownTestFolder(legacyConfigDir);
});

afterEach(async () => {
await teardownTestFolder(oldVersionOutputDir);
await teardownTestFolder(flatConfigOutputDir);
await teardownTestFolder(legacyConfigOutputDir);
});

it('should run ESLint plugin for flat config and create report.json', async () => {
const { code, stderr } = await executeProcess({
command: 'npx',
args: ['@code-pushup/cli', 'collect', '--no-progress'],
cwd: flatConfigDir,
});

expect(code).toBe(0);
expect(stderr).toBe('');

const report = await readJsonFile(join(flatConfigOutputDir, 'report.json'));

expect(() => reportSchema.parse(report)).not.toThrow();
expect(omitVariableReportData(report as Report)).toMatchSnapshot();
});

it('should run ESLint plugin and create report.json', async () => {
it('should run ESLint plugin for legacy config and create report.json', async () => {
const { code, stderr } = await executeProcess({
command: 'npx',
args: ['@code-pushup/cli', 'collect', '--no-progress'],
cwd: oldVersionDir,
cwd: legacyConfigDir,
env: { ...process.env, ESLINT_USE_FLAT_CONFIG: 'false' },
});

expect(code).toBe(0);
expect(stderr).toBe('');

const report = await readJsonFile(join(oldVersionOutputDir, 'report.json'));
const report = await readJsonFile(
join(legacyConfigOutputDir, 'report.json'),
);

expect(() => reportSchema.parse(report)).not.toThrow();
expect(omitVariableReportData(report as Report)).toMatchSnapshot();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
root: true
extends: '@code-pushup'
4 changes: 2 additions & 2 deletions packages/plugin-eslint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
"dependencies": {
"@code-pushup/utils": "0.54.0",
"@code-pushup/models": "0.54.0",
"eslint": "^8.46.0",
"zod": "^3.22.4"
},
"peerDependencies": {
"@nx/devkit": ">=17.0.0"
"@nx/devkit": ">=17.0.0",
"eslint": "^8.46.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"@nx/devkit": {
Expand Down
13 changes: 2 additions & 11 deletions packages/plugin-eslint/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import type { ESLint } from 'eslint';
import { type ZodType, z } from 'zod';
import { z } from 'zod';
import { toArray } from '@code-pushup/utils';

const patternsSchema = z.union([z.string(), z.array(z.string()).min(1)], {
description:
'Lint target files. May contain file paths, directory paths or glob patterns',
});

const eslintrcSchema = z.union(
[
z.string({ description: 'Path to ESLint config file' }),
z.record(z.string(), z.unknown(), {
description: 'ESLint config object',
}) as ZodType<ESLint.ConfigData>,
],
{ description: 'ESLint config as file path or inline object' },
);
const eslintrcSchema = z.string({ description: 'Path to ESLint config file' });

const eslintTargetObjectSchema = z.object({
eslintrc: eslintrcSchema.optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,6 @@ describe('eslintPlugin', () => {
);
});

it('should initialize ESLint plugin using inline config', async () => {
cwdSpy.mockReturnValue(thisDir);
const plugin = await eslintPlugin({
eslintrc: {
extends: '@code-pushup',
},
patterns: '**/*.ts',
});

expect(plugin.groups?.length).toBeGreaterThanOrEqual(3);
expect(plugin.audits.length).toBeGreaterThanOrEqual(200);
});

it('should throw when invalid parameters provided', async () => {
await expect(
// @ts-expect-error simulating invalid non-TS config
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-eslint/src/lib/meta/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Rule } from 'eslint';
import type { Group, GroupRef } from '@code-pushup/models';
import { objectToKeys, slugify } from '@code-pushup/utils';
import { ruleIdToSlug } from './hash';
import { type RuleData, parseRuleId } from './rules';
import { type RuleData, parseRuleId } from './parse';

type RuleType = NonNullable<Rule.RuleMetaData['type']>;

Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-eslint/src/lib/meta/groups.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Group } from '@code-pushup/models';
import { groupsFromRuleCategories, groupsFromRuleTypes } from './groups';
import type { RuleData } from './rules';
import type { RuleData } from './parse';

const eslintRules: RuleData[] = [
{
Expand Down
Loading