From effd5d2603c68f33a18081355dcca85f7bd24b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Fri, 29 Nov 2024 11:55:33 +0100 Subject: [PATCH] feat(plugin-eslint): support new config format in nx helpers --- packages/plugin-eslint/src/lib/meta/index.ts | 2 + .../src/lib/nx.integration.test.ts | 40 +- .../src/lib/nx/projects-to-config.ts | 21 +- .../lib/nx/projects-to-config.unit.test.ts | 74 ++-- packages/plugin-eslint/src/lib/nx/utils.ts | 110 ++++- .../src/lib/nx/utils.unit.test.ts | 404 +++++++++++++++--- 6 files changed, 507 insertions(+), 144 deletions(-) diff --git a/packages/plugin-eslint/src/lib/meta/index.ts b/packages/plugin-eslint/src/lib/meta/index.ts index 1252c8778..41355406c 100644 --- a/packages/plugin-eslint/src/lib/meta/index.ts +++ b/packages/plugin-eslint/src/lib/meta/index.ts @@ -4,6 +4,8 @@ import { groupsFromRuleCategories, groupsFromRuleTypes } from './groups'; import { listRules } from './rules'; import { ruleToAudit } from './transform'; +export { detectConfigVersion, type ConfigFormat } from './versions'; + export async function listAuditsAndGroups( targets: ESLintTarget[], ): Promise<{ audits: Audit[]; groups: Group[] }> { diff --git a/packages/plugin-eslint/src/lib/nx.integration.test.ts b/packages/plugin-eslint/src/lib/nx.integration.test.ts index 0c19b77c8..1395b3374 100644 --- a/packages/plugin-eslint/src/lib/nx.integration.test.ts +++ b/packages/plugin-eslint/src/lib/nx.integration.test.ts @@ -46,10 +46,10 @@ describe('Nx helpers', () => { patterns: [ 'packages/cli/**/*.ts', 'packages/cli/package.json', - 'packages/cli/src/*.spec.ts', - 'packages/cli/src/*.cy.ts', - 'packages/cli/src/*.stories.ts', - 'packages/cli/src/.storybook/main.ts', + 'packages/cli/*.spec.ts', + 'packages/cli/*.cy.ts', + 'packages/cli/*.stories.ts', + 'packages/cli/.storybook/main.ts', ], }, { @@ -57,10 +57,10 @@ describe('Nx helpers', () => { patterns: [ 'packages/core/**/*.ts', 'packages/core/package.json', - 'packages/core/src/*.spec.ts', - 'packages/core/src/*.cy.ts', - 'packages/core/src/*.stories.ts', - 'packages/core/src/.storybook/main.ts', + 'packages/core/*.spec.ts', + 'packages/core/*.cy.ts', + 'packages/core/*.stories.ts', + 'packages/core/.storybook/main.ts', ], }, { @@ -69,10 +69,10 @@ describe('Nx helpers', () => { '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', + 'packages/nx-plugin/*.spec.ts', + 'packages/nx-plugin/*.cy.ts', + 'packages/nx-plugin/*.stories.ts', + 'packages/nx-plugin/.storybook/main.ts', ], }, { @@ -80,10 +80,10 @@ describe('Nx helpers', () => { patterns: [ 'packages/utils/**/*.ts', 'packages/utils/package.json', - 'packages/utils/src/*.spec.ts', - 'packages/utils/src/*.cy.ts', - 'packages/utils/src/*.stories.ts', - 'packages/utils/src/.storybook/main.ts', + 'packages/utils/*.spec.ts', + 'packages/utils/*.cy.ts', + 'packages/utils/*.stories.ts', + 'packages/utils/.storybook/main.ts', ], }, ] satisfies ESLintTarget[]); @@ -99,10 +99,10 @@ describe('Nx helpers', () => { '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', + 'packages/nx-plugin/*.spec.ts', + 'packages/nx-plugin/*.cy.ts', + 'packages/nx-plugin/*.stories.ts', + 'packages/nx-plugin/.storybook/main.ts', ], }, ] satisfies ESLintTarget[]); diff --git a/packages/plugin-eslint/src/lib/nx/projects-to-config.ts b/packages/plugin-eslint/src/lib/nx/projects-to-config.ts index 02bd699bd..6fcad1ac4 100644 --- a/packages/plugin-eslint/src/lib/nx/projects-to-config.ts +++ b/packages/plugin-eslint/src/lib/nx/projects-to-config.ts @@ -1,8 +1,9 @@ import type { ProjectConfiguration, ProjectGraph } from '@nx/devkit'; import type { ESLintTarget } from '../config'; +import { detectConfigVersion } from '../meta'; import { - findCodePushupEslintrc, - getEslintConfig, + findCodePushupEslintConfig, + findEslintConfig, getLintFilePatterns, } from './utils'; @@ -21,21 +22,15 @@ export async function nxProjectsToConfig( .filter(predicate) // apply predicate .sort((a, b) => a.root.localeCompare(b.root)); + const format = await detectConfigVersion(); + return Promise.all( projects.map( async (project): Promise => ({ eslintrc: - (await findCodePushupEslintrc(project)) ?? getEslintConfig(project), - patterns: [ - ...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 - ], + (await findCodePushupEslintConfig(project, format)) ?? + (await findEslintConfig(project, format)), + patterns: getLintFilePatterns(project, format), }), ), ); 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 ae4c12197..09323f327 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 @@ -25,18 +25,9 @@ describe('nxProjectsToConfig', () => { const config = await nxProjectsToConfig(projectGraph); expect(config).toEqual([ - { - eslintrc: './apps/client/.eslintrc.json', - patterns: expect.arrayContaining(['apps/client/**/*.ts']), - }, - { - eslintrc: './apps/server/.eslintrc.json', - patterns: expect.arrayContaining(['apps/server/**/*.ts']), - }, - { - eslintrc: './libs/models/.eslintrc.json', - patterns: expect.arrayContaining(['libs/models/**/*.ts']), - }, + { patterns: expect.arrayContaining(['apps/client/**/*.ts']) }, + { patterns: expect.arrayContaining(['apps/server/**/*.ts']) }, + { patterns: expect.arrayContaining(['libs/models/**/*.ts']) }, ]); }); @@ -65,10 +56,7 @@ describe('nxProjectsToConfig', () => { ); expect(config).toEqual([ - { - eslintrc: './libs/models/.eslintrc.json', - patterns: expect.arrayContaining(['libs/models/**/*.ts']), - }, + { patterns: expect.arrayContaining(['libs/models/**/*.ts']) }, ]); }); @@ -107,18 +95,13 @@ describe('nxProjectsToConfig', () => { const config = await nxProjectsToConfig(projectGraph); expect(config).toEqual([ - { - eslintrc: './apps/client/.eslintrc.json', - patterns: expect.arrayContaining(['apps/client/**/*.ts']), - }, - { - eslintrc: './apps/server/.eslintrc.json', - patterns: expect.arrayContaining(['apps/server/**/*.ts']), - }, + { patterns: expect.arrayContaining(['apps/client/**/*.ts']) }, + { patterns: expect.arrayContaining(['apps/server/**/*.ts']) }, ]); }); - it('should use code-pushup.eslintrc.json if available', async () => { + it('should use code-pushup.eslintrc.json if available and using legacy config', async () => { + vi.stubEnv('ESLINT_USE_FLAT_CONFIG', 'false'); vol.fromJSON( { 'apps/client/code-pushup.eslintrc.json': @@ -139,6 +122,45 @@ describe('nxProjectsToConfig', () => { ]); }); + it('should use eslint.strict.config.js if available and using flat config', async () => { + vi.stubEnv('ESLINT_USE_FLAT_CONFIG', 'true'); + vol.fromJSON( + { + 'apps/client/eslint.strict.config.js': 'export default [/*...*/]', + }, + MEMFS_VOLUME, + ); + const projectGraph = toProjectGraph([ + { name: 'client', type: 'app', data: { root: 'apps/client' } }, + ]); + + const config = await nxProjectsToConfig(projectGraph); + + expect(config).toEqual([ + expect.objectContaining>({ + eslintrc: './apps/client/eslint.strict.config.js', + }), + ]); + }); + + it('should NOT use code-pushup.eslintrc.json if available but using flat config', async () => { + vi.stubEnv('ESLINT_USE_FLAT_CONFIG', 'true'); + vol.fromJSON( + { + 'apps/client/code-pushup.eslintrc.json': + '{ "eslintrc": "@code-pushup" }', + }, + MEMFS_VOLUME, + ); + const projectGraph = toProjectGraph([ + { name: 'client', type: 'app', data: { root: 'apps/client' } }, + ]); + + const config = await nxProjectsToConfig(projectGraph); + + expect(config[0]!.eslintrc).toBeUndefined(); + }); + it("should use each project's lint file patterns", async () => { const projectGraph = toProjectGraph([ { @@ -176,14 +198,12 @@ describe('nxProjectsToConfig', () => { await expect(nxProjectsToConfig(projectGraph)).resolves.toEqual([ { - eslintrc: './apps/client/.eslintrc.json', patterns: expect.arrayContaining([ 'apps/client/**/*.ts', 'apps/client/**/*.html', ]), }, { - eslintrc: './apps/server/.eslintrc.json', patterns: expect.arrayContaining(['apps/server/**/*.ts']), }, ] satisfies ESLintPluginConfig); diff --git a/packages/plugin-eslint/src/lib/nx/utils.ts b/packages/plugin-eslint/src/lib/nx/utils.ts index 2169d90f1..6723ddc69 100644 --- a/packages/plugin-eslint/src/lib/nx/utils.ts +++ b/packages/plugin-eslint/src/lib/nx/utils.ts @@ -1,39 +1,105 @@ import type { ProjectConfiguration } from '@nx/devkit'; import { join } from 'node:path'; import { fileExists, toArray } from '@code-pushup/utils'; +import type { ConfigFormat } from '../meta'; -export async function findCodePushupEslintrc( - project: ProjectConfiguration, -): Promise { - const name = 'code-pushup.eslintrc'; +const ESLINT_CONFIG_EXTENSIONS: Record = { + // https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file-formats + flat: ['js', 'mjs', 'cjs'], + // https://eslint.org/docs/latest/use/configure/configuration-files-deprecated + legacy: ['json', 'js', 'cjs', 'yml', 'yaml'], +}; +const ESLINT_CONFIG_NAMES: Record = { // https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file-formats - const extensions = ['json', 'js', 'cjs', 'yml', 'yaml']; + flat: ['eslint.config'], + // https://eslint.org/docs/latest/use/configure/configuration-files-deprecated + legacy: ['.eslintrc'], +}; - // eslint-disable-next-line functional/no-loop-statements - for (const ext of extensions) { - const filename = `./${project.root}/${name}.${ext}`; - if (await fileExists(join(process.cwd(), filename))) { - return filename; - } - } +const CP_ESLINT_CONFIG_NAMES: Record = { + flat: [ + 'code-pushup.eslint.config', + 'eslint.code-pushup.config', + 'eslint.config.code-pushup', + 'eslint.strict.config', + 'eslint.config.strict', + ], + legacy: ['code-pushup.eslintrc', '.eslintrc.code-pushup', '.eslintrc.strict'], +}; - return null; +export async function findCodePushupEslintConfig( + project: ProjectConfiguration, + format: ConfigFormat, +): Promise { + return findProjectFile(project, { + names: CP_ESLINT_CONFIG_NAMES[format], + extensions: ESLINT_CONFIG_EXTENSIONS[format], + }); } -export function getLintFilePatterns(project: ProjectConfiguration): string[] { +export async function findEslintConfig( + project: ProjectConfiguration, + format: ConfigFormat, +): Promise { const options = project.targets?.['lint']?.options as - | { lintFilePatterns?: string | string[] } + | { eslintConfig?: string } | undefined; - return options?.lintFilePatterns == null - ? [`${project.root}/**/*`] // lintFilePatterns defaults to ["{projectRoot}"] - https://github.com/nrwl/nx/pull/20313 - : toArray(options.lintFilePatterns); + return ( + options?.eslintConfig ?? + (await findProjectFile(project, { + names: ESLINT_CONFIG_NAMES[format], + extensions: ESLINT_CONFIG_EXTENSIONS[format], + })) + ); } -export function getEslintConfig( +export function getLintFilePatterns( project: ProjectConfiguration, -): string | undefined { + format: ConfigFormat, +): string[] { const options = project.targets?.['lint']?.options as - | { eslintConfig?: string } + | { lintFilePatterns?: string | string[] } | undefined; - return options?.eslintConfig ?? `./${project.root}/.eslintrc.json`; + // lintFilePatterns defaults to ["{projectRoot}"] - https://github.com/nrwl/nx/pull/20313 + const defaultPatterns = + format === 'legacy' + ? `${project.root}/**/*` // files not folder needed for legacy because rules detected with ESLint.calculateConfigForFile + : project.root; + const patterns = + options?.lintFilePatterns == null + ? [defaultPatterns] + : toArray(options.lintFilePatterns); + if (format === 'legacy') { + return [ + ...patterns, + // 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 is only necessary for legacy configs (rules are detected more reliably in flat configs) + `${project.root}/*.spec.ts`, // jest/* and vitest/* rules + `${project.root}/*.cy.ts`, // cypress/* rules + `${project.root}/*.stories.ts`, // storybook/* rules + `${project.root}/.storybook/main.ts`, // storybook/no-uninstalled-addons rule + ]; + } + return patterns; +} + +async function findProjectFile( + project: ProjectConfiguration, + file: { + names: string[]; + extensions: string[]; + }, +): Promise { + // eslint-disable-next-line functional/no-loop-statements + for (const name of file.names) { + // eslint-disable-next-line functional/no-loop-statements + for (const ext of file.extensions) { + const filename = `./${project.root}/${name}.${ext}`; + if (await fileExists(join(process.cwd(), filename))) { + return filename; + } + } + } + return undefined; } diff --git a/packages/plugin-eslint/src/lib/nx/utils.unit.test.ts b/packages/plugin-eslint/src/lib/nx/utils.unit.test.ts index 0ab2b3bee..c39423f6c 100644 --- a/packages/plugin-eslint/src/lib/nx/utils.unit.test.ts +++ b/packages/plugin-eslint/src/lib/nx/utils.unit.test.ts @@ -1,86 +1,366 @@ import { vol } from 'memfs'; -import type { MockInstance } from 'vitest'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; -import { findCodePushupEslintrc } from './utils'; +import { + findCodePushupEslintConfig, + findEslintConfig, + getLintFilePatterns, +} from './utils'; -describe('find code-pushup.eslintrc.* file', () => { - let cwdSpy: MockInstance<[], string>; +describe('findCodePushupEslintConfig', () => { + describe('flat config format', () => { + const fileContent = `import javascript from '@code-pushup/eslint-config/javascript';\n\nexport default javascript;\n`; - beforeAll(() => { - cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(MEMFS_VOLUME); - }); + it('should find code-pushup.eslint.config.js', async () => { + vol.fromJSON( + { + 'packages/cli/code-pushup.eslint.config.js': fileContent, + 'packages/core/code-pushup.eslint.config.js': fileContent, + }, + MEMFS_VOLUME, + ); + + await expect( + findCodePushupEslintConfig({ root: 'packages/cli' }, 'flat'), + ).resolves.toBe('./packages/cli/code-pushup.eslint.config.js'); + }); + + it('should find eslint.config.code-pushup.mjs', async () => { + vol.fromJSON( + { + 'packages/cli/eslint.config.code-pushup.mjs': fileContent, + 'packages/core/eslint.config.code-pushup.mjs': fileContent, + }, + MEMFS_VOLUME, + ); + + await expect( + findCodePushupEslintConfig({ root: 'packages/core' }, 'flat'), + ).resolves.toBe('./packages/core/eslint.config.code-pushup.mjs'); + }); + + it('should find eslint.strict.config.js', async () => { + vol.fromJSON( + { + 'packages/cli/eslint.strict.config.mjs': fileContent, + 'packages/core/eslint.strict.config.js': fileContent, + }, + MEMFS_VOLUME, + ); + + await expect( + findCodePushupEslintConfig({ root: 'packages/core' }, 'flat'), + ).resolves.toBe('./packages/core/eslint.strict.config.js'); + }); + + it('should return undefined if no config exists', async () => { + vol.fromJSON({}, MEMFS_VOLUME); - afterAll(() => { - cwdSpy.mockRestore(); + await expect( + findCodePushupEslintConfig({ root: 'libs/utils' }, 'flat'), + ).resolves.toBeUndefined(); + }); + + it('should return undefined if only standard config exists', async () => { + vol.fromJSON( + { 'libs/utils/eslint.config.js': fileContent }, + MEMFS_VOLUME, + ); + + await expect( + findCodePushupEslintConfig({ root: 'libs/utils' }, 'flat'), + ).resolves.toBeUndefined(); + }); }); - it('should find code-pushup.eslinrc.json', async () => { - vol.fromJSON( - { - 'packages/cli/code-pushup.eslintrc.json': - '{ "extends": "@code-pushup" }', - 'packages/core/code-pushup.eslintrc.json': - '{ "extends": "@code-pushup" }', - }, - MEMFS_VOLUME, - ); + describe('legacy config format', () => { + it('should find code-pushup.eslintrc.json', async () => { + vol.fromJSON( + { + 'packages/cli/code-pushup.eslintrc.json': + '{ "extends": "@code-pushup" }', + 'packages/core/code-pushup.eslintrc.json': + '{ "extends": "@code-pushup" }', + }, + MEMFS_VOLUME, + ); + + await expect( + findCodePushupEslintConfig({ root: 'packages/cli' }, 'legacy'), + ).resolves.toBe('./packages/cli/code-pushup.eslintrc.json'); + }); + + it('should find .eslintrc.code-pushup.json', async () => { + vol.fromJSON( + { + 'packages/cli/.eslintrc.code-pushup.json': + "module.exports = { extends: '@code-pushup' };", + 'packages/core/.eslintrc.code-pushup.json': + "module.exports = { extends: '@code-pushup' };", + }, + MEMFS_VOLUME, + ); + + await expect( + findCodePushupEslintConfig({ root: 'packages/core' }, 'legacy'), + ).resolves.toBe('./packages/core/.eslintrc.code-pushup.json'); + }); + it('should return undefined if no config exists', async () => { + vol.fromJSON({}, MEMFS_VOLUME); + + await expect( + findCodePushupEslintConfig({ root: 'libs/utils' }, 'legacy'), + ).resolves.toBeUndefined(); + }); + + it('should return undefined if only standard config exists', async () => { + vol.fromJSON( + { + 'libs/utils/.eslintrc.json': + '{ "extends": "@code-pushup/eslint-config" }', + }, + MEMFS_VOLUME, + ); + + await expect( + findCodePushupEslintConfig({ root: 'libs/utils' }, 'legacy'), + ).resolves.toBeUndefined(); + }); + }); +}); + +describe('findEslintConfig', () => { + it('should use eslintConfig from project.json if configured', async () => { await expect( - findCodePushupEslintrc({ root: 'packages/cli' }), - ).resolves.toBe('./packages/cli/code-pushup.eslintrc.json'); + findEslintConfig( + { + root: 'packages/cli', + targets: { + lint: { + options: { eslintConfig: 'packages/cli/eslint.ci.config.js' }, + }, + }, + }, + 'flat', + ), + ).resolves.toBe('packages/cli/eslint.ci.config.js'); }); - it('should find code-pushup.eslinrc.js', async () => { - vol.fromJSON( - { - 'packages/cli/code-pushup.eslintrc.json': - '{ "extends": "@code-pushup" }', - 'packages/core/code-pushup.eslintrc.js': - "module.exports = { extends: '@code-pushup' };", - }, - MEMFS_VOLUME, - ); + describe('flat config format', () => { + const fileContent = `import javascript from '@code-pushup/eslint-config/javascript';\n\nexport default javascript;\n`; - await expect( - findCodePushupEslintrc({ root: 'packages/core' }), - ).resolves.toBe('./packages/core/code-pushup.eslintrc.js'); + it('should find eslint.config.js', async () => { + vol.fromJSON( + { + 'packages/cli/eslint.config.js': fileContent, + 'packages/core/eslint.config.js': fileContent, + }, + MEMFS_VOLUME, + ); + + await expect( + findEslintConfig({ root: 'packages/cli' }, 'flat'), + ).resolves.toBe('./packages/cli/eslint.config.js'); + }); + + it('should find eslint.config.mjs', async () => { + vol.fromJSON( + { + 'packages/cli/eslint.config.mjs': fileContent, + 'packages/core/eslint.config.mjs': fileContent, + }, + MEMFS_VOLUME, + ); + + await expect( + findEslintConfig({ root: 'packages/core' }, 'flat'), + ).resolves.toBe('./packages/core/eslint.config.mjs'); + }); + + it('should look for JS extension before MJS', async () => { + vol.fromJSON( + { + 'libs/utils/eslint.config.js': fileContent, + 'libs/utils/eslint.config.mjs': fileContent, + }, + MEMFS_VOLUME, + ); + + await expect( + findEslintConfig({ root: 'libs/utils' }, 'flat'), + ).resolves.toBe('./libs/utils/eslint.config.js'); + }); + + it('should return undefined if no config exists', async () => { + vol.fromJSON({}, MEMFS_VOLUME); + + await expect( + findEslintConfig({ root: 'libs/utils' }, 'flat'), + ).resolves.toBeUndefined(); + }); + + it('should return undefined if only legacy config exists', async () => { + vol.fromJSON( + { 'libs/utils/.eslintrc.json': '{ "extends": "@code-pushup" }' }, + MEMFS_VOLUME, + ); + + await expect( + findEslintConfig({ root: 'libs/utils' }, 'flat'), + ).resolves.toBeUndefined(); + }); }); - it('should look for JSON extension before JavaScript', async () => { - vol.fromJSON( - { - 'libs/utils/code-pushup.eslintrc.js': - "module.exports = { extends: '@code-pushup' };", - 'libs/utils/code-pushup.eslintrc.json': '{ "extends": "@code-pushup" }', - }, - MEMFS_VOLUME, - ); + describe('legacy config format', () => { + it('should find .eslintrc.json', async () => { + vol.fromJSON( + { + 'packages/cli/.eslintrc.json': '{ "extends": "@code-pushup" }', + 'packages/core/.eslintrc.json': '{ "extends": "@code-pushup" }', + }, + MEMFS_VOLUME, + ); - await expect(findCodePushupEslintrc({ root: 'libs/utils' })).resolves.toBe( - './libs/utils/code-pushup.eslintrc.json', - ); + await expect( + findEslintConfig({ root: 'packages/cli' }, 'legacy'), + ).resolves.toBe('./packages/cli/.eslintrc.json'); + }); + + it('should find .eslintrc.js', async () => { + vol.fromJSON( + { + 'packages/cli/.eslintrc.json': '{ "extends": "@code-pushup" }', + 'packages/core/.eslintrc.js': + "module.exports = { extends: '@code-pushup' };", + }, + MEMFS_VOLUME, + ); + + await expect( + findEslintConfig({ root: 'packages/core' }, 'legacy'), + ).resolves.toBe('./packages/core/.eslintrc.js'); + }); + + it('should look for JSON extension before JavaScript', async () => { + vol.fromJSON( + { + 'libs/utils/.eslintrc.js': + "module.exports = { extends: '@code-pushup' };", + 'libs/utils/.eslintrc.json': '{ "extends": "@code-pushup" }', + }, + MEMFS_VOLUME, + ); + + await expect( + findEslintConfig({ root: 'libs/utils' }, 'legacy'), + ).resolves.toBe('./libs/utils/.eslintrc.json'); + }); + + it('should look for JavaScript extensions before YAML', async () => { + vol.fromJSON( + { + 'libs/utils/.eslintrc.cjs': + "module.exports = { extends: '@code-pushup' };", + 'libs/utils/.eslintrc.yml': '- extends: "@code-pushup"\n', + }, + MEMFS_VOLUME, + ); + + await expect( + findEslintConfig({ root: 'libs/utils' }, 'legacy'), + ).resolves.toBe('./libs/utils/.eslintrc.cjs'); + }); + + it('should return undefined if no config exists', async () => { + vol.fromJSON({}, MEMFS_VOLUME); + + await expect( + findEslintConfig({ root: 'libs/utils' }, 'legacy'), + ).resolves.toBeUndefined(); + }); + + it('should return undefined if only flat config exists', async () => { + vol.fromJSON( + { 'libs/utils/eslint.config.js': 'export default [/*...*/]' }, + MEMFS_VOLUME, + ); + + await expect( + findEslintConfig({ root: 'libs/utils' }, 'legacy'), + ).resolves.toBeUndefined(); + }); + }); +}); + +describe('getLintFilePatterns', () => { + it('should get lintFilePatterns from project.json if configured', () => { + expect( + getLintFilePatterns( + { + root: 'apps/website', + targets: { + lint: { + options: { + lintFilePatterns: [ + 'apps/website/**/*.ts', + 'apps/website/**/*.html', + ], + }, + }, + }, + }, + 'flat', + ), + ).toEqual(['apps/website/**/*.ts', 'apps/website/**/*.html']); }); - it('should look for JavaScript extensions before YAML', async () => { - vol.fromJSON( - { - 'libs/utils/code-pushup.eslintrc.cjs': - "module.exports = { extends: '@code-pushup' };", - 'libs/utils/code-pushup.eslintrc.yml': '- extends: "@code-pushup"\n', - }, - MEMFS_VOLUME, - ); + it('should default to project root folder if using flat config', () => { + expect(getLintFilePatterns({ root: 'apps/website' }, 'flat')).toEqual([ + 'apps/website', + ]); + }); - await expect(findCodePushupEslintrc({ root: 'libs/utils' })).resolves.toBe( - './libs/utils/code-pushup.eslintrc.cjs', + it('should default to all files in project root folder if using legacy config', () => { + expect(getLintFilePatterns({ root: 'apps/website' }, 'legacy')).toEqual( + expect.arrayContaining(['apps/website/**/*']), ); }); - it('should return null if not found', async () => { - vol.fromJSON({}, MEMFS_VOLUME); + it('should add additional patterns to defaults if using legacy config', () => { + expect(getLintFilePatterns({ root: 'apps/website' }, 'legacy')).toEqual([ + 'apps/website/**/*', + 'apps/website/*.spec.ts', + 'apps/website/*.cy.ts', + 'apps/website/*.stories.ts', + 'apps/website/.storybook/main.ts', + ]); + }); - await expect( - findCodePushupEslintrc({ root: 'libs/utils' }), - ).resolves.toBeNull(); + it('should add additional patterns to lintFilePatterns if using legacy config', () => { + expect( + getLintFilePatterns( + { + root: 'apps/website', + targets: { + lint: { + options: { + lintFilePatterns: [ + 'apps/website/**/*.ts', + 'apps/website/**/*.html', + ], + }, + }, + }, + }, + 'legacy', + ), + ).toEqual([ + 'apps/website/**/*.ts', + 'apps/website/**/*.html', + 'apps/website/*.spec.ts', + 'apps/website/*.cy.ts', + 'apps/website/*.stories.ts', + 'apps/website/.storybook/main.ts', + ]); }); });