From e9560f5695a7d9876870ef9bec7e5d58bc2c0a34 Mon Sep 17 00:00:00 2001 From: Hanna Skryl <80118140+hanna-skryl@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:08:30 -0400 Subject: [PATCH] feat(plugin-eslint): add exclude option for Nx projects --- packages/plugin-eslint/README.md | 6 ++ .../src/lib/nx.integration.test.ts | 19 ++++++ .../src/lib/nx/filter-project-graph.ts | 31 +++++++++ .../lib/nx/filter-project-graph.unit.test.ts | 68 +++++++++++++++++++ .../src/lib/nx/find-all-projects.ts | 20 ++++-- .../lib/nx/projects-to-config.unit.test.ts | 44 +----------- testing/test-utils/src/index.ts | 1 + .../test-utils/src/lib/utils/project-graph.ts | 51 ++++++++++++++ 8 files changed, 193 insertions(+), 47 deletions(-) create mode 100644 packages/plugin-eslint/src/lib/nx/filter-project-graph.ts create mode 100644 packages/plugin-eslint/src/lib/nx/filter-project-graph.unit.test.ts create mode 100644 testing/test-utils/src/lib/utils/project-graph.ts diff --git a/packages/plugin-eslint/README.md b/packages/plugin-eslint/README.md index e0c14a80a..83e9252de 100644 --- a/packages/plugin-eslint/README.md +++ b/packages/plugin-eslint/README.md @@ -72,6 +72,12 @@ Detected ESLint rules are mapped to Code PushUp audits. Audit reports are calcul }; ``` + You can also exclude specific projects if needed by passing their names in the `exclude` option: + + ```js + await eslintConfigFromAllNxProjects({ exclude: ['server'] }); + ``` + - If you wish to target a specific project along with other projects it depends on, use the `eslintConfigFromNxProjectAndDeps` helper and pass in in your project name: ```js diff --git a/packages/plugin-eslint/src/lib/nx.integration.test.ts b/packages/plugin-eslint/src/lib/nx.integration.test.ts index 0d13fafab..0c19b77c8 100644 --- a/packages/plugin-eslint/src/lib/nx.integration.test.ts +++ b/packages/plugin-eslint/src/lib/nx.integration.test.ts @@ -88,6 +88,25 @@ describe('Nx helpers', () => { }, ] satisfies ESLintTarget[]); }); + + it('should exclude specified projects and return only eslintrc and patterns of a remaining project', async () => { + await expect( + eslintConfigFromAllNxProjects({ exclude: ['cli', 'core', 'utils'] }), + ).resolves.toEqual([ + { + eslintrc: './packages/nx-plugin/.eslintrc.json', + patterns: [ + 'packages/nx-plugin/**/*.ts', + 'packages/nx-plugin/package.json', + 'packages/nx-plugin/generators.json', + 'packages/nx-plugin/src/*.spec.ts', + 'packages/nx-plugin/src/*.cy.ts', + 'packages/nx-plugin/src/*.stories.ts', + 'packages/nx-plugin/src/.storybook/main.ts', + ], + }, + ] satisfies ESLintTarget[]); + }); }); describe('create config from target Nx project and its dependencies', () => { diff --git a/packages/plugin-eslint/src/lib/nx/filter-project-graph.ts b/packages/plugin-eslint/src/lib/nx/filter-project-graph.ts new file mode 100644 index 000000000..aac0ae405 --- /dev/null +++ b/packages/plugin-eslint/src/lib/nx/filter-project-graph.ts @@ -0,0 +1,31 @@ +import type { + ProjectGraph, + ProjectGraphDependency, + ProjectGraphProjectNode, +} from '@nx/devkit'; + +export function filterProjectGraph( + projectGraph: ProjectGraph, + exclude: string[] = [], +): ProjectGraph { + const filteredNodes: Record = Object.entries( + projectGraph.nodes, + ).reduce( + (acc, [projectName, projectNode]) => + exclude.includes(projectName) + ? acc + : { ...acc, [projectName]: projectNode }, + {}, + ); + const filteredDependencies: Record = + Object.entries(projectGraph.dependencies).reduce( + (acc, [key, deps]) => + exclude.includes(key) ? acc : { ...acc, [key]: deps }, + {}, + ); + return { + nodes: filteredNodes, + dependencies: filteredDependencies, + version: projectGraph.version, + }; +} diff --git a/packages/plugin-eslint/src/lib/nx/filter-project-graph.unit.test.ts b/packages/plugin-eslint/src/lib/nx/filter-project-graph.unit.test.ts new file mode 100644 index 000000000..291ef7edc --- /dev/null +++ b/packages/plugin-eslint/src/lib/nx/filter-project-graph.unit.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import { toProjectGraph } from '@code-pushup/test-utils'; +import { filterProjectGraph } from './filter-project-graph'; + +describe('filterProjectGraph', () => { + it('should exclude specified projects from nodes', () => { + const projectGraph = toProjectGraph([ + { name: 'client', type: 'app', data: { root: 'apps/client' } }, + { name: 'server', type: 'app', data: { root: 'apps/server' } }, + { name: 'models', type: 'lib', data: { root: 'libs/models' } }, + ]); + + const filteredGraph = filterProjectGraph(projectGraph, ['client']); + + expect(Object.keys(filteredGraph.nodes)).not.toContain('client'); + expect(Object.keys(filteredGraph.nodes)).toContain('server'); + expect(Object.keys(filteredGraph.nodes)).toContain('models'); + }); + + it('should exclude dependencies of excluded projects', () => { + const projectGraph = toProjectGraph( + [ + { name: 'client', type: 'app', data: { root: 'apps/client' } }, + { name: 'server', type: 'app', data: { root: 'apps/server' } }, + { name: 'models', type: 'lib', data: { root: 'libs/models' } }, + ], + { + client: ['server'], + server: ['models'], + }, + ); + + const filteredGraph = filterProjectGraph(projectGraph, ['client']); + + expect(Object.keys(filteredGraph.dependencies)).not.toContain('client'); + expect(filteredGraph.dependencies['server']).toEqual([ + { source: 'server', target: 'models', type: 'static' }, + ]); + }); + + it('should include all projects if exclude list is empty', () => { + const projectGraph = toProjectGraph([ + { name: 'client', type: 'app', data: { root: 'apps/client' } }, + { name: 'server', type: 'app', data: { root: 'apps/server' } }, + { name: 'models', type: 'lib', data: { root: 'libs/models' } }, + ]); + + const filteredGraph = filterProjectGraph(projectGraph, []); + + expect(Object.keys(filteredGraph.nodes)).toContain('client'); + expect(Object.keys(filteredGraph.nodes)).toContain('server'); + expect(Object.keys(filteredGraph.nodes)).toContain('models'); + }); + + it('should ignore non-existent projects in the exclude list', () => { + const projectGraph = toProjectGraph([ + { name: 'client', type: 'app', data: { root: 'apps/client' } }, + { name: 'server', type: 'app', data: { root: 'apps/server' } }, + { name: 'models', type: 'lib', data: { root: 'libs/models' } }, + ]); + + const filteredGraph = filterProjectGraph(projectGraph, ['non-existent']); + + expect(Object.keys(filteredGraph.nodes)).toContain('client'); + expect(Object.keys(filteredGraph.nodes)).toContain('server'); + expect(Object.keys(filteredGraph.nodes)).toContain('models'); + }); +}); diff --git a/packages/plugin-eslint/src/lib/nx/find-all-projects.ts b/packages/plugin-eslint/src/lib/nx/find-all-projects.ts index 076dc4f23..d33e20a1d 100644 --- a/packages/plugin-eslint/src/lib/nx/find-all-projects.ts +++ b/packages/plugin-eslint/src/lib/nx/find-all-projects.ts @@ -1,11 +1,15 @@ import type { ESLintTarget } from '../config'; +import { filterProjectGraph } from './filter-project-graph'; import { nxProjectsToConfig } from './projects-to-config'; /** * Finds all Nx projects in workspace and converts their lint configurations to Code PushUp ESLint plugin parameters. * + * Allows excluding certain projects from the configuration using the `options.exclude` parameter. + * * Use when you wish to automatically include every Nx project in a single Code PushUp project. - * If you prefer to only include a subset of your Nx monorepo, refer to {@link eslintConfigFromNxProjectAndDeps} instead. + * If you prefer to include only a subset of your Nx monorepo, specify projects to exclude using the `exclude` option + * or consider using {@link eslintConfigFromNxProjectAndDeps} for finer control. * * @example * import eslintPlugin, { @@ -15,17 +19,25 @@ import { nxProjectsToConfig } from './projects-to-config'; * export default { * plugins: [ * await eslintPlugin( - * await eslintConfigFromAllNxProjects() + * await eslintConfigFromAllNxProjects({ exclude: ['server'] }) * ) * ] * } * + * @param options - Configuration options to filter projects + * @param options.exclude - Array of project names to exclude from the ESLint configuration * @returns ESLint config and patterns, intended to be passed to {@link eslintPlugin} */ -export async function eslintConfigFromAllNxProjects(): Promise { +export async function eslintConfigFromAllNxProjects( + options: { exclude?: string[] } = {}, +): Promise { const { createProjectGraphAsync } = await import('@nx/devkit'); const projectGraph = await createProjectGraphAsync({ exitOnError: false }); - return nxProjectsToConfig(projectGraph); + const filteredProjectGraph = filterProjectGraph( + projectGraph, + options.exclude, + ); + return nxProjectsToConfig(filteredProjectGraph); } /** diff --git a/packages/plugin-eslint/src/lib/nx/projects-to-config.unit.test.ts b/packages/plugin-eslint/src/lib/nx/projects-to-config.unit.test.ts index 5d6da8532..ae4c12197 100644 --- a/packages/plugin-eslint/src/lib/nx/projects-to-config.unit.test.ts +++ b/packages/plugin-eslint/src/lib/nx/projects-to-config.unit.test.ts @@ -1,52 +1,10 @@ -import type { - ProjectGraph, - ProjectGraphDependency, - ProjectGraphProjectNode, -} from '@nx/devkit'; import { vol } from 'memfs'; import type { MockInstance } from 'vitest'; -import { MEMFS_VOLUME } from '@code-pushup/test-utils'; +import { MEMFS_VOLUME, toProjectGraph } from '@code-pushup/test-utils'; import type { ESLintPluginConfig, ESLintTarget } from '../config'; import { nxProjectsToConfig } from './projects-to-config'; describe('nxProjectsToConfig', () => { - const toProjectGraph = ( - nodes: ProjectGraphProjectNode[], - dependencies?: Record, - ): ProjectGraph => ({ - nodes: Object.fromEntries( - nodes.map(node => [ - node.name, - { - ...node, - data: { - targets: { - lint: { - options: { - lintFilePatterns: `${node.data.root}/**/*.ts`, - }, - }, - }, - sourceRoot: `${node.data.root}/src`, - ...node.data, - }, - }, - ]), - ), - dependencies: Object.fromEntries( - nodes.map(node => [ - node.name, - dependencies?.[node.name]?.map( - (target): ProjectGraphDependency => ({ - source: node.name, - target, - type: 'static', - }), - ) ?? [], - ]), - ), - }); - let cwdSpy: MockInstance<[], string>; beforeAll(() => { diff --git a/testing/test-utils/src/index.ts b/testing/test-utils/src/index.ts index e8d5e8319..4ada76af9 100644 --- a/testing/test-utils/src/index.ts +++ b/testing/test-utils/src/index.ts @@ -8,6 +8,7 @@ export * from './lib/utils/string'; export * from './lib/utils/file-system'; export * from './lib/utils/create-npm-workshpace'; export * from './lib/utils/omit-report-data'; +export * from './lib/utils/project-graph'; // static mocks export * from './lib/utils/commit.mock'; diff --git a/testing/test-utils/src/lib/utils/project-graph.ts b/testing/test-utils/src/lib/utils/project-graph.ts new file mode 100644 index 000000000..7ade6e782 --- /dev/null +++ b/testing/test-utils/src/lib/utils/project-graph.ts @@ -0,0 +1,51 @@ +import type { + ProjectGraph, + ProjectGraphDependency, + ProjectGraphProjectNode, +} from '@nx/devkit'; + +/** + * Converts nodes and dependencies into a ProjectGraph object for testing purposes. + * + * @param nodes - Array of ProjectGraphProjectNode representing project nodes. + * @param dependencies - Optional dependencies for each project in a record format. + * @returns A ProjectGraph object. + */ +export function toProjectGraph( + nodes: ProjectGraphProjectNode[], + dependencies?: Record, +): ProjectGraph { + return { + nodes: Object.fromEntries( + nodes.map(node => [ + node.name, + { + ...node, + data: { + targets: { + lint: { + options: { + lintFilePatterns: `${node.data.root}/**/*.ts`, + }, + }, + }, + sourceRoot: `${node.data.root}/src`, + ...node.data, + }, + }, + ]), + ), + dependencies: Object.fromEntries( + nodes.map(node => [ + node.name, + dependencies?.[node.name]?.map( + (target): ProjectGraphDependency => ({ + source: node.name, + target, + type: 'static', + }), + ) ?? [], + ]), + ), + }; +}