Skip to content

Commit

Permalink
feat(plugin-eslint): add exclude option for Nx projects
Browse files Browse the repository at this point in the history
  • Loading branch information
hanna-skryl authored Oct 29, 2024
1 parent d8a7eb2 commit e9560f5
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 47 deletions.
6 changes: 6 additions & 0 deletions packages/plugin-eslint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions packages/plugin-eslint/src/lib/nx.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
31 changes: 31 additions & 0 deletions packages/plugin-eslint/src/lib/nx/filter-project-graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type {
ProjectGraph,
ProjectGraphDependency,
ProjectGraphProjectNode,
} from '@nx/devkit';

export function filterProjectGraph(
projectGraph: ProjectGraph,
exclude: string[] = [],
): ProjectGraph {
const filteredNodes: Record<string, ProjectGraphProjectNode> = Object.entries(
projectGraph.nodes,
).reduce(
(acc, [projectName, projectNode]) =>
exclude.includes(projectName)
? acc
: { ...acc, [projectName]: projectNode },
{},
);
const filteredDependencies: Record<string, ProjectGraphDependency[]> =
Object.entries(projectGraph.dependencies).reduce(
(acc, [key, deps]) =>
exclude.includes(key) ? acc : { ...acc, [key]: deps },
{},
);
return {
nodes: filteredNodes,
dependencies: filteredDependencies,
version: projectGraph.version,
};
}
Original file line number Diff line number Diff line change
@@ -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');
});
});
20 changes: 16 additions & 4 deletions packages/plugin-eslint/src/lib/nx/find-all-projects.ts
Original file line number Diff line number Diff line change
@@ -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, {
Expand All @@ -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<ESLintTarget[]> {
export async function eslintConfigFromAllNxProjects(
options: { exclude?: string[] } = {},
): Promise<ESLintTarget[]> {
const { createProjectGraphAsync } = await import('@nx/devkit');
const projectGraph = await createProjectGraphAsync({ exitOnError: false });
return nxProjectsToConfig(projectGraph);
const filteredProjectGraph = filterProjectGraph(
projectGraph,
options.exclude,
);
return nxProjectsToConfig(filteredProjectGraph);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string[]>,
): 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(() => {
Expand Down
1 change: 1 addition & 0 deletions testing/test-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
51 changes: 51 additions & 0 deletions testing/test-utils/src/lib/utils/project-graph.ts
Original file line number Diff line number Diff line change
@@ -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<string, string[]>,
): 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',
}),
) ?? [],
]),
),
};
}

0 comments on commit e9560f5

Please sign in to comment.