Skip to content

Commit

Permalink
feat(plugin-eslint): provide Nx helper to combine eslint configs from…
Browse files Browse the repository at this point in the history
… project with deps
  • Loading branch information
matejchalk committed Dec 4, 2023
1 parent 6c1edb0 commit 29cd887
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 47 deletions.
5 changes: 4 additions & 1 deletion packages/plugin-eslint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ export default eslintPlugin;

export type { ESLintPluginConfig } from './lib/config';

export { eslintConfigFromNxProjects } from './lib/nx';
export {
eslintConfigFromNxProject,
eslintConfigFromNxProjects,
} from './lib/nx';
56 changes: 55 additions & 1 deletion packages/plugin-eslint/src/lib/nx.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import type { SpyInstance } from 'vitest';
import { ESLintPluginConfig } from './config';
import { eslintConfigFromNxProjects } from './nx';
import { eslintConfigFromNxProject, eslintConfigFromNxProjects } from './nx';

describe('Nx helpers', () => {
let cwdSpy: SpyInstance;
Expand Down Expand Up @@ -89,4 +89,58 @@ describe('Nx helpers', () => {
} satisfies ESLintPluginConfig);
});
});

describe('create config from target Nx project and its dependencies', () => {
/*
* Project graph:
*
* cli
* │
* │
* ▼
* core
* │ nx-plugin
* │ │
* ▼ │
* utils ◄──────┘
*/

const allProjects = ['cli', 'core', 'nx-plugin', 'utils'] as const;
type Project = (typeof allProjects)[number];

it.each<[Project, Project[]]>([
['cli', ['cli', 'core', 'utils']],
['core', ['core', 'utils']],
['nx-plugin', ['nx-plugin', 'utils']],
['utils', ['utils']],
])(
'project %j - expected configurations for projects %j',
async (project, expectedProjects) => {
const otherProjects = allProjects.filter(
p => !expectedProjects.includes(p),
);

const config = await eslintConfigFromNxProject(project);

expect(config.eslintrc).toEqual({
root: true,
overrides: expectedProjects.map(p => ({
files: expect.arrayContaining([`packages/${p}/**/*.ts`]),
extends: `./packages/${p}/.eslintrc.json`,
})),
});

expect(config.patterns).toEqual(
expect.arrayContaining(
expectedProjects.map(p => `packages/${p}/**/*.ts`),
),
);
expect(config.patterns).toEqual(
expect.not.arrayContaining(
otherProjects.map(p => `packages/${p}/**/*.ts`),
),
);
},
);
});
});
69 changes: 24 additions & 45 deletions packages/plugin-eslint/src/lib/nx/find-all-projects.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,29 @@
import {
createProjectGraphAsync,
readProjectsConfigurationFromProjectGraph,
} from '@nx/devkit';
import type { ESLint } from 'eslint';
import { createProjectGraphAsync } from '@nx/devkit';
import type { ESLintPluginConfig } from '../config';
import {
findCodePushupEslintrc,
getEslintConfig,
getLintFilePatterns,
} from './utils';
import { nxProjectsToConfig } from './projects-to-config';

/**
* Finds all Nx projects in workspace and converts their lint configurations to Code PushUp ESLint plugin parameters.
*
* 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 eslintConfigFromNxProject} instead.
*
* @example
* import eslintPlugin, {
* eslintConfigFromNxProjects,
* } from '@code-pushup/eslint-plugin';
*
* export default {
* plugins: [
* await eslintPlugin(
* await eslintConfigFromNxProjects()
* )
* ]
* }
*
* @returns ESLint config and patterns, intended to be passed to {@link eslintPlugin}
*/
export async function eslintConfigFromNxProjects(): Promise<ESLintPluginConfig> {
// find Nx projects with lint target
const projectGraph = await createProjectGraphAsync({ exitOnError: false });
const projectsConfiguration =
readProjectsConfigurationFromProjectGraph(projectGraph);
const projects = Object.values(projectsConfiguration.projects)
.filter(project => 'lint' in (project.targets ?? {}))
.sort((a, b) => a.root.localeCompare(b.root));

// create single ESLint config with project-specific overrides
const eslintConfig: ESLint.ConfigData = {
root: true,
overrides: await Promise.all(
projects.map(async project => ({
files: getLintFilePatterns(project),
extends:
(await findCodePushupEslintrc(project)) ?? getEslintConfig(project),
})),
),
};

// include patterns from each project
const patterns = projects.flatMap(project => [
...getLintFilePatterns(project),
// HACK: ESLint.calculateConfigForFile won't find rules included only for subsets of *.ts when globs used
// so we explicitly provide additional patterns used by @code-pushup/eslint-config to ensure those rules are included
// this workaround won't be necessary once flat configs are stable (much easier to find all rules)
`${project.sourceRoot}/*.spec.ts`, // jest/* and vitest/* rules
`${project.sourceRoot}/*.cy.ts`, // cypress/* rules
`${project.sourceRoot}/*.stories.ts`, // storybook/* rules
`${project.sourceRoot}/.storybook/main.ts`, // storybook/no-uninstalled-addons rule
]);

return {
eslintrc: eslintConfig,
patterns,
};
return nxProjectsToConfig(projectGraph);
}
68 changes: 68 additions & 0 deletions packages/plugin-eslint/src/lib/nx/find-project-with-deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ProjectGraph, createProjectGraphAsync } from '@nx/devkit';
import type { ESLintPluginConfig } from '../config';
import { nxProjectsToConfig } from './projects-to-config';

/**
* Accepts a target Nx projects, finds projects it depends on, and converts lint configurations to Code PushUp ESLint plugin parameters.
*
* Use when you wish to include a targetted subset of your Nx monorepo in your Code PushUp project.
* If you prefer to include all Nx projects, refer to {@link eslintConfigFromNxProjects} instead.
*
* @example
* import eslintPlugin, {
* eslintConfigFromNxProject,
* } from '@code-pushup/eslint-plugin';
*
* const projectName = 'backoffice'; // <-- name from project.json
*
* export default {
* plugins: [
* await eslintPlugin(
* await eslintConfigFromNxProject(projectName)
* )
* ]
* }
*
* @param projectName Nx project serving as main entry point
* @returns ESLint config and patterns, intended to be passed to {@link eslintPlugin}
*/
export async function eslintConfigFromNxProject(
projectName: string,
): Promise<ESLintPluginConfig> {
const projectGraph = await createProjectGraphAsync({ exitOnError: false });

const dependencies = findAllDependencies(projectName, projectGraph);

return nxProjectsToConfig(
projectGraph,
project =>
!!project.name &&
(project.name === projectName || dependencies.has(project.name)),
);
}

function findAllDependencies(
name: string,
projectGraph: ProjectGraph,
): ReadonlySet<string> {
const results = new Set<string>();
const queue = [name];

// eslint-disable-next-line functional/no-loop-statements
while (queue.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const source = queue.shift()!;
const dependencies = projectGraph.dependencies[source];

// eslint-disable-next-line functional/no-loop-statements
for (const { target } of dependencies ?? []) {
// skip duplicates (cycle in graph)
if (!results.has(target)) {
results.add(target);
queue.push(target);
}
}
}

return results;
}
1 change: 1 addition & 0 deletions packages/plugin-eslint/src/lib/nx/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { eslintConfigFromNxProjects } from './find-all-projects';
export { eslintConfigFromNxProject } from './find-project-with-deps';
54 changes: 54 additions & 0 deletions packages/plugin-eslint/src/lib/nx/projects-to-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
ProjectConfiguration,
ProjectGraph,
readProjectsConfigurationFromProjectGraph,
} from '@nx/devkit';
import type { ESLint } from 'eslint';
import type { ESLintPluginConfig } from '../config';
import {
findCodePushupEslintrc,
getEslintConfig,
getLintFilePatterns,
} from './utils';

export async function nxProjectsToConfig(
projectGraph: ProjectGraph,
predicate: (project: ProjectConfiguration) => boolean = () => true,
): Promise<ESLintPluginConfig> {
// find Nx projects with lint target
const projectsConfiguration =
readProjectsConfigurationFromProjectGraph(projectGraph);
const projects = Object.values(projectsConfiguration.projects)
.filter(project => 'lint' in (project.targets ?? {}))
.filter(predicate) // apply predicate
.sort((a, b) => a.root.localeCompare(b.root));

// create single ESLint config with project-specific overrides
const eslintConfig: ESLint.ConfigData = {
root: true,
overrides: await Promise.all(
projects.map(async project => ({
files: getLintFilePatterns(project),
extends:
(await findCodePushupEslintrc(project)) ?? getEslintConfig(project),
})),
),
};

// include patterns from each project
const patterns = projects.flatMap(project => [
...getLintFilePatterns(project),
// HACK: ESLint.calculateConfigForFile won't find rules included only for subsets of *.ts when globs used
// so we explicitly provide additional patterns used by @code-pushup/eslint-config to ensure those rules are included
// this workaround won't be necessary once flat configs are stable (much easier to find all rules)
`${project.sourceRoot}/*.spec.ts`, // jest/* and vitest/* rules
`${project.sourceRoot}/*.cy.ts`, // cypress/* rules
`${project.sourceRoot}/*.stories.ts`, // storybook/* rules
`${project.sourceRoot}/.storybook/main.ts`, // storybook/no-uninstalled-addons rule
]);

return {
eslintrc: eslintConfig,
patterns,
};
}

0 comments on commit 29cd887

Please sign in to comment.