From 7c738deced7bd42259db1d06a3fa0b5c3fc74531 Mon Sep 17 00:00:00 2001 From: Geoffrey Testelin Date: Mon, 20 Nov 2023 12:14:18 +0100 Subject: [PATCH] feat: allow running the tests with vitest The executor will be read to define what configuration is expected in order to read the right path for the coverage directory. --- README.md | 2 +- .../src/executors/scan/executor-jest.spec.ts | 321 +++++++++++++ .../src/executors/scan/executor-vite.spec.ts | 431 ++++++++++++++++++ .../src/executors/scan/executor.spec.ts | 94 +--- .../src/executors/scan/executor.ts | 9 +- .../src/executors/scan/utils/utils.ts | 94 +++- 6 files changed, 860 insertions(+), 91 deletions(-) create mode 100644 packages/nx-sonarqube/src/executors/scan/executor-jest.spec.ts create mode 100644 packages/nx-sonarqube/src/executors/scan/executor-vite.spec.ts diff --git a/README.md b/README.md index 248e266..abec2c6 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ lcov Paths: 1. Nx workspace 2. SonarQube or Sonar Cloud instance -3. Jest tests & code coverage enabled +3. Jest/Vite tests & code coverage enabled ### Installation diff --git a/packages/nx-sonarqube/src/executors/scan/executor-jest.spec.ts b/packages/nx-sonarqube/src/executors/scan/executor-jest.spec.ts new file mode 100644 index 0000000..179502b --- /dev/null +++ b/packages/nx-sonarqube/src/executors/scan/executor-jest.spec.ts @@ -0,0 +1,321 @@ +import sonarScanExecutor from './executor'; +import { + DependencyType, + ExecutorContext, + ProjectGraph, + readJsonFile, +} from '@nx/devkit'; +import * as sonarQubeScanner from 'sonarqube-scanner'; +import * as fs from 'fs'; +import { determinePaths } from './utils/utils'; + +let projectGraph: ProjectGraph; +let context: ExecutorContext; + +class MockError extends Error {} + +jest.mock('@nx/devkit', () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...jest.requireActual('@nx/devkit'), + readCachedProjectGraph: jest.fn().mockImplementation(() => { + throw new Error('readCachedProjectGraph error'); + }), + createProjectGraphAsync: jest + .fn() + .mockImplementation(async () => projectGraph), + readJsonFile: jest.fn().mockImplementation(() => { + throw new MockError('not implemented for this test'); + }), +})); + +jest.mock('sonarqube-scanner'); + +describe('Scan Executor', (): void => { + let jestConfig: string; + + beforeEach((): void => { + (readJsonFile as jest.MockedFunction).mockReset(); + + context = { + cwd: '', + isVerbose: false, + root: '', + projectName: 'app1', + workspace: { + version: 2, + projects: { + app1: { + root: 'apps/app1', + sourceRoot: 'apps/app1/src', + targets: { + test: { + executor: '', + options: { + jestConfig: 'jest.config.ts', + }, + }, + }, + }, + lib1: { + root: 'libs/lib1', + sourceRoot: 'libs/lib1/src', + targets: { + test: { + executor: '@nx/jest:jest', + options: { + jestConfig: 'jest.config.ts', + }, + }, + }, + }, + lib2: { + root: 'libs/lib2', + sourceRoot: 'libs/lib2/src', + targets: { + test: { + executor: '@nx/jest:jest', + options: { + jestConfig: 'jest.config.ts', + }, + }, + }, + }, + lib3: { + root: 'libs/lib3', + sourceRoot: 'libs/lib3/src', + targets: { + test: { + executor: '@nx/jest:jest', + options: { + jestConfig: 'jest.config.ts', + }, + }, + }, + }, + }, + }, + }; + + projectGraph = { + dependencies: { + app1: [ + { + type: DependencyType.static, + source: 'app1', + target: 'lib1', + }, + { + type: DependencyType.static, + source: 'app1', + target: 'lib2', + }, + { + type: DependencyType.implicit, + source: 'app1', + target: 'lib3', + }, + ], + lib1: [ + { + type: DependencyType.static, + source: 'lib1', + target: 'lib2', + }, + { + type: DependencyType.implicit, + source: 'lib1', + target: 'lib3', + }, + ], + lib2: [ + { + type: DependencyType.static, + source: 'lib2', + target: 'lib3', + }, + ], + }, + nodes: { + app1: { + name: 'app1', + type: 'app', + data: { + root: 'apps/app1', + sourceRoot: 'apps/app1/src', + targets: { + test: { + executor: '', + options: { + jestConfig: 'jest.config.ts', + }, + }, + }, + }, + }, + lib1: { + name: 'lib1', + type: 'lib', + data: { + root: 'libs/lib1', + sourceRoot: 'libs/lib1/src', + targets: { + test: { + executor: '', + options: { + jestConfig: 'jest.config.ts', + }, + }, + }, + }, + }, + lib2: { + name: 'lib2', + type: 'lib', + data: { + root: 'libs/lib2', + sourceRoot: 'libs/lib2/src', + targets: { + test: { + executor: '', + options: { + jestConfig: 'jest.config.ts', + }, + }, + }, + }, + }, + lib3: { + name: 'lib3', + type: 'lib', + data: { + root: 'libs/lib3', + sourceRoot: 'libs/lib3/src', + targets: { + test: { + executor: '', + options: { + jestConfig: 'jest.config.ts', + }, + }, + }, + }, + }, + }, + }; + + jestConfig = `export default { + displayName: 'app1', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\\\.[tj]s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'html', 'json'], + coverageDirectory: '../../coverage/apps/app1', + };`; + }); + + afterEach((): void => { + jest.clearAllMocks(); + }); + + + it('should scan project and dependencies & skip projects with no test target', async () => { + jest.spyOn(fs, 'readFileSync').mockReturnValue(jestConfig); + sonarQubeScanner.mockResolvedValue(true); + + const newContext = { ...context }; + newContext.workspace.projects['app1'].targets = {}; + + const output = await sonarScanExecutor( + { + hostUrl: 'url', + projectKey: 'key', + qualityGate: true, + }, + newContext + ); + expect(output.success).toBe(true); + }); + + it('should scan project and dependencies & skip projects with no jestConfig', async () => { + jest.spyOn(fs, 'readFileSync').mockReturnValue(jestConfig); + sonarQubeScanner.mockResolvedValue(true); + + const newContext = { ...context }; + newContext.workspace.projects['app1'].targets.test.options = {}; + + const output = await sonarScanExecutor( + { + hostUrl: 'url', + projectKey: 'key', + qualityGate: true, + }, + newContext + ); + expect(output.success).toBe(true); + }); + + it('should scan project and dependencies & skip projects with no coverageDirectory', async () => { + jest.spyOn(fs, 'readFileSync').mockReturnValue(''); + + sonarQubeScanner.mockResolvedValue(true); + + const output = await sonarScanExecutor( + { + hostUrl: 'url', + projectKey: 'key', + qualityGate: true, + }, + context + ); + expect(output.success).toBe(true); + }); + + it('should error on sonar scanner issue', async () => { + jest.spyOn(fs, 'readFileSync').mockReturnValue(jestConfig); + sonarQubeScanner.async.mockImplementation(() => { + throw new Error(); + }); + + const output = await sonarScanExecutor( + { + hostUrl: 'url', + projectKey: 'key', + }, + context + ); + expect(output.success).toBeFalsy(); + }); + + it('should return jest config coverage directory path', async () => { + const paths = await determinePaths( + { + hostUrl: 'url', + projectKey: 'key', + }, + context + ); + expect(paths.lcovPaths.includes('coverage/apps/app1/lcov.info')).toBe(true); + }); + + it('should return project test config coverage directory path', async () => { + const testContext = JSON.parse(JSON.stringify(context)) as typeof context; + testContext.workspace.projects.app1.targets.test.options.coverageDirectory = + 'coverage/test/apps/app1'; + const paths = await determinePaths( + { + hostUrl: 'url', + projectKey: 'key', + }, + testContext + ); + expect(paths.lcovPaths.includes('coverage/test/apps/app1/lcov.info')).toBe( + true + ); + }); +}); diff --git a/packages/nx-sonarqube/src/executors/scan/executor-vite.spec.ts b/packages/nx-sonarqube/src/executors/scan/executor-vite.spec.ts new file mode 100644 index 0000000..67e7ae3 --- /dev/null +++ b/packages/nx-sonarqube/src/executors/scan/executor-vite.spec.ts @@ -0,0 +1,431 @@ +import sonarScanExecutor from './executor'; +import { + DependencyType, + ExecutorContext, + ProjectGraph, + readJsonFile, +} from '@nx/devkit'; +import * as sonarQubeScanner from 'sonarqube-scanner'; +import { determinePaths } from './utils/utils'; +import { ScanExecutorSchema } from "./schema"; +import * as fs from "fs"; + +let projectGraph: ProjectGraph; +let context: ExecutorContext; + +class MockError extends Error {} + +jest.mock('@nx/devkit', () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...jest.requireActual('@nx/devkit'), + readCachedProjectGraph: jest.fn().mockImplementation(() => { + throw new Error('readCachedProjectGraph error'); + }), + createProjectGraphAsync: jest + .fn() + .mockImplementation(async () => projectGraph), + readJsonFile: jest.fn().mockImplementation(() => { + throw new MockError('not implemented for this test'); + }), +})); + +jest.mock('sonarqube-scanner'); + +describe('Scan Executor', (): void => { + let viteConfig: string; + let commonOptions: Partial & Pick; + + beforeEach((): void => { + (readJsonFile as jest.MockedFunction).mockReset(); + + context = { + cwd: '', + isVerbose: false, + root: '', + projectName: 'app1', + workspace: { + version: 2, + projects: { + app1: { + root: 'apps/app1', + sourceRoot: 'apps/app1/src', + targets: { + test: { + executor: '@nx/vite:test', + options: { + reportsDirectory: "../../coverage/apps/app1" + }, + }, + }, + }, + lib1: { + root: 'libs/lib1', + sourceRoot: 'libs/lib1/src', + targets: { + test: { + executor: '@nx/vite:test', + options: { + reportsDirectory: "../../coverage/apps/app1" + }, + }, + }, + }, + lib2: { + root: 'libs/lib2', + sourceRoot: 'libs/lib2/src', + targets: { + test: { + executor: '@nx/vite:test', + options: { + reportsDirectory: "../../coverage/apps/app1" + }, + }, + }, + }, + lib3: { + root: 'libs/lib3', + sourceRoot: 'libs/lib3/src', + targets: { + test: { + executor: '@nx/vite:test', + options: { + reportsDirectory: "../../coverage/apps/app1" + }, + }, + }, + }, + }, + }, + }; + + projectGraph = { + dependencies: { + app1: [ + { + type: DependencyType.static, + source: 'app1', + target: 'lib1', + }, + { + type: DependencyType.static, + source: 'app1', + target: 'lib2', + }, + { + type: DependencyType.implicit, + source: 'app1', + target: 'lib3', + }, + ], + lib1: [ + { + type: DependencyType.static, + source: 'lib1', + target: 'lib2', + }, + { + type: DependencyType.implicit, + source: 'lib1', + target: 'lib3', + }, + ], + lib2: [ + { + type: DependencyType.static, + source: 'lib2', + target: 'lib3', + }, + ], + }, + nodes: { + app1: { + name: 'app1', + type: 'app', + data: { + root: 'apps/app1', + sourceRoot: 'apps/app1/src', + targets: { + test: { + executor: '', + }, + }, + }, + }, + lib1: { + name: 'lib1', + type: 'lib', + data: { + root: 'libs/lib1', + sourceRoot: 'libs/lib1/src', + targets: { + test: { + executor: '', + }, + }, + }, + }, + lib2: { + name: 'lib2', + type: 'lib', + data: { + root: 'libs/lib2', + sourceRoot: 'libs/lib2/src', + targets: { + test: { + executor: '', + }, + }, + }, + }, + lib3: { + name: 'lib3', + type: 'lib', + data: { + root: 'libs/lib3', + sourceRoot: 'libs/lib3/src', + targets: { + test: { + executor: '', + }, + }, + }, + }, + }, + }; + commonOptions = { + hostUrl: 'url', + projectKey: 'key', + }; + viteConfig = ` + import { defineConfig, UserConfig } from 'vite'; + + export default defineConfig( + (): UserConfig => ({ + test: { + coverage: { + reportsDirectory: '../../vite-coverage/apps/app1', + }, + }, + }), + ); + `; + }); + + afterEach((): void => { + jest.clearAllMocks(); + }); + + it('should scan project and dependencies & skip projects with no test target', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true) + jest.spyOn(fs, 'readFileSync').mockReturnValue(viteConfig); + sonarQubeScanner.mockResolvedValue(true); + + const newContext = { ...context }; + newContext.workspace.projects['app1'].targets = {}; + + const output = await sonarScanExecutor( + { + ...commonOptions, + qualityGate: true, + }, + newContext + ); + expect(output.success).toBe(true); + }); + + it('should scan project and dependencies & skip projects with no vitest config', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true) + jest.spyOn(fs, 'readFileSync').mockReturnValue(viteConfig); + sonarQubeScanner.mockResolvedValue(true); + + const newContext = { ...context }; + newContext.workspace.projects['app1'].targets.test.options = {}; + + const output = await sonarScanExecutor( + { + ...commonOptions, + qualityGate: true, + }, + newContext + ); + expect(output.success).toBe(true); + }); + + it('should scan project and dependencies & skip projects with no reportsDirectory', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true) + jest.spyOn(fs, 'readFileSync').mockReturnValue(viteConfig) + sonarQubeScanner.mockResolvedValue(true); + + const newContext = { ...context }; + newContext.workspace.projects['app1'].targets.test.options = { + coverage: true + }; + + const output = await sonarScanExecutor( + { + ...commonOptions, + qualityGate: true, + }, + context + ); + expect(output.success).toBe(true); + }); + + it('should scan project and dependencies & skip projects with no vite config file', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(false) + sonarQubeScanner.mockResolvedValue(true); + + const newContext = { ...context }; + newContext.workspace.projects['app1'].targets.test.options = { + coverage: true + }; + + const output = await sonarScanExecutor( + { + ...commonOptions, + qualityGate: true, + }, + context + ); + expect(output.success).toBe(true); + }); + + it('should scan project and dependencies & skip projects with a vite config file without a reportsDirectory', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true) + jest.spyOn(fs, 'readFileSync').mockReturnValue(` + import { defineConfig, UserConfig } from 'vite'; + + export default defineConfig( + (): UserConfig => ({ + test: { + coverage: {}, + }, + }), + ); + `) + sonarQubeScanner.mockResolvedValue(true); + + const newContext = { ...context }; + newContext.workspace.projects['app1'].targets.test.options = { + coverage: true + }; + + const output = await sonarScanExecutor( + { + ...commonOptions, + qualityGate: true, + }, + context + ); + expect(output.success).toBe(true); + }); + + it('should error on sonar scanner issue', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true) + jest.spyOn(fs, 'readFileSync').mockReturnValue(viteConfig) + sonarQubeScanner.async.mockImplementation(() => { + throw new Error(); + }); + + const output = await sonarScanExecutor( + commonOptions, + context + ); + expect(output.success).toBe(false); + }); + + it('should return vitest config coverage directory path', async () => { + const paths = await determinePaths( + commonOptions, + context + ); + expect(paths.lcovPaths.includes('coverage/apps/app1/lcov.info')).toBe(true); + }); + + it('should return project test config coverage directory path (from the options)', async () => { + const testContext = JSON.parse(JSON.stringify(context)) as typeof context; + testContext.workspace.projects.app1.targets.test.options.reportsDirectory = + 'coverage/test/apps/app1'; + const paths = await determinePaths( + commonOptions, + testContext + ); + expect(paths.lcovPaths.includes('coverage/test/apps/app1/lcov.info')).toBe( + true + ); + }); + + it('should return project test config coverage directory path (from the option "configFile")', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true) + jest.spyOn(fs, 'readFileSync').mockReturnValue(` + import { defineConfig, UserConfig } from 'vite'; + + export default defineConfig( + (): UserConfig => ({ + test: { + coverage: { + reportsDirectory: '../../vite-custom-coverage/apps/app1', + }, + }, + }), + ); + `) + const testContext = JSON.parse(JSON.stringify(context)) as typeof context; + testContext.workspace.projects.app1.targets.test.options = { + configFile: '../../vite-custom-coverage/apps/app1' + }; + + const paths = await determinePaths( + commonOptions, + testContext + ); + expect(paths.lcovPaths.includes('vite-custom-coverage/apps/app1/lcov.info')).toBe( + true + ); + }); + + it('should return project test config coverage directory path (from the project vite config file)', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValue(true) + jest.spyOn(fs, 'readFileSync').mockReturnValue(viteConfig) + const testContext = JSON.parse(JSON.stringify(context)) as typeof context; + testContext.workspace.projects.app1.targets.test.options = undefined; + + const paths = await determinePaths( + commonOptions, + testContext + ); + expect(paths.lcovPaths.includes('vite-coverage/apps/app1/lcov.info')).toBe( + true + ); + }); + + it('should return project test config coverage directory path (from the root vite.config.ts file)', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false).mockReturnValue(true) + jest.spyOn(fs, 'readFileSync').mockReturnValue(viteConfig) + const testContext = JSON.parse(JSON.stringify(context)) as typeof context; + testContext.workspace.projects.app1.targets.test.options = {}; + + const paths = await determinePaths( + commonOptions, + testContext + ); + expect(paths.lcovPaths.includes('vite-coverage/apps/app1/lcov.info')).toBe( + true + ); + }); + + it('should return project test config coverage directory path (from the root vite.config.js file)', async () => { + jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false).mockReturnValue(true) + jest.spyOn(fs, 'readFileSync').mockReturnValue(viteConfig) + const testContext = JSON.parse(JSON.stringify(context)) as typeof context; + testContext.workspace.projects.app1.targets.test.options = {}; + + const paths = await determinePaths( + commonOptions, + testContext + ); + expect(paths.lcovPaths.includes('vite-coverage/apps/app1/lcov.info')).toBe( + true + ); + }); +}); diff --git a/packages/nx-sonarqube/src/executors/scan/executor.spec.ts b/packages/nx-sonarqube/src/executors/scan/executor.spec.ts index 95ea763..10add29 100644 --- a/packages/nx-sonarqube/src/executors/scan/executor.spec.ts +++ b/packages/nx-sonarqube/src/executors/scan/executor.spec.ts @@ -8,9 +8,11 @@ import { import * as sonarQubeScanner from 'sonarqube-scanner'; import * as childProcess from 'child_process'; import * as fs from 'fs'; -import { determinePaths, getScannerOptions } from './utils/utils'; +import { getScannerOptions } from './utils/utils'; + let projectGraph: ProjectGraph; let context: ExecutorContext; + class MockError extends Error {} jest.mock('@nx/devkit', () => ({ @@ -29,10 +31,13 @@ jest.mock('@nx/devkit', () => ({ jest.mock('sonarqube-scanner'); -describe('Scan Executor', () => { +describe('Scan Executor', (): void => { let jestConfig: string; let defaultPackageJson: string; - beforeEach(() => { + + beforeEach((): void => { + (readJsonFile as jest.MockedFunction).mockReset(); + context = { cwd: '', isVerbose: false, @@ -218,10 +223,9 @@ describe('Scan Executor', () => { moduleFileExtensions: ['ts', 'js', 'html', 'json'], coverageDirectory: '../../coverage/apps/app1', };`; - (readJsonFile as jest.MockedFunction).mockReset(); }); - afterEach(() => { + afterEach((): void => { jest.clearAllMocks(); }); @@ -277,81 +281,6 @@ describe('Scan Executor', () => { expect(output.success).toBe(true); }); - it('should scan project and dependencies & skip projects with no jestConfig', async () => { - jest.spyOn(fs, 'readFileSync').mockReturnValue(jestConfig); - sonarQubeScanner.mockResolvedValue(true); - - const newContext = { ...context }; - newContext.workspace.projects['app1'].targets.test.options = {}; - - const output = await sonarScanExecutor( - { - hostUrl: 'url', - projectKey: 'key', - qualityGate: true, - }, - newContext - ); - expect(output.success).toBe(true); - }); - - it('should scan project and dependencies & skip projects with no coverageDirectory', async () => { - jest.spyOn(fs, 'readFileSync').mockReturnValue(''); - - sonarQubeScanner.mockResolvedValue(true); - - const output = await sonarScanExecutor( - { - hostUrl: 'url', - projectKey: 'key', - qualityGate: true, - }, - context - ); - expect(output.success).toBe(true); - }); - - it('should error on sonar scanner issue', async () => { - jest.spyOn(fs, 'readFileSync').mockReturnValue(jestConfig); - sonarQubeScanner.async.mockImplementation(() => { - throw new Error(); - }); - - const output = await sonarScanExecutor( - { - hostUrl: 'url', - projectKey: 'key', - }, - context - ); - expect(output.success).toBeFalsy(); - }); - it('should return jest config coverage directory path', async () => { - const paths = await determinePaths( - { - hostUrl: 'url', - projectKey: 'key', - }, - context - ); - expect(paths.lcovPaths.includes('coverage/apps/app1/lcov.info')).toBe(true); - }); - it('should return project test config coverage directory path', async () => { - const testContext = JSON.parse(JSON.stringify(context)) as typeof context; - testContext.workspace.projects.app1.targets.test.options.coverageDirectory = - 'coverage/test/apps/app1'; - const paths = await determinePaths( - { - hostUrl: 'url', - projectKey: 'key', - }, - testContext - ); - expect(paths.lcovPaths.includes('coverage/test/apps/app1/lcov.info')).toBe( - true - ); - }); - it('should override environment variable over options over extra ', async () => { ( readJsonFile as jest.MockedFunction @@ -388,6 +317,7 @@ describe('Scan Executor', () => { expect(output['sonar.log.level.extended']).toBe('DEBUG'); expect(output['sonar.test.inclusions']).toBe('include'); }); + it('should return app package json', async () => { const packageJson = { version: '1.1.1', @@ -423,6 +353,7 @@ describe('Scan Executor', () => { expect(output['sonar.projectVersion']).toBe(packageJson.version); }); + it('should return root package json', async () => { const packageJson = { version: '2.1.2', @@ -458,6 +389,7 @@ describe('Scan Executor', () => { expect(output['sonar.projectVersion']).toBe(packageJson.version); }); + it('should return options version', async () => { const packageVersion = '3.3.3'; ( @@ -487,6 +419,7 @@ describe('Scan Executor', () => { expect(output['sonar.projectVersion']).toBe(packageVersion); }); + it('should return no version', async () => { ( readJsonFile as jest.MockedFunction @@ -512,6 +445,7 @@ describe('Scan Executor', () => { expect(output['sonar.projectVersion']).toBe(''); }); + it('should return no version 2', async () => { ( readJsonFile as jest.MockedFunction diff --git a/packages/nx-sonarqube/src/executors/scan/executor.ts b/packages/nx-sonarqube/src/executors/scan/executor.ts index 1ebc6ea..4c613c0 100644 --- a/packages/nx-sonarqube/src/executors/scan/executor.ts +++ b/packages/nx-sonarqube/src/executors/scan/executor.ts @@ -1,18 +1,19 @@ import { ScanExecutorSchema } from './schema'; import { ExecutorContext, logger } from '@nx/devkit'; - import { scanner } from './utils/utils'; export default async function ( options: ScanExecutorSchema, context: ExecutorContext ): Promise<{ success: boolean }> { - let success = true; + let success: boolean = true; - await scanner(options, context).catch((e) => { + await scanner(options, context).catch((e): void => { logger.error( - `The SonarQube scan failed for project '${context.projectName}'. Error: ${e}` + `The SonarQube scan failed for project '${context.projectName}'` ); + logger.error(e); + success = false; }); diff --git a/packages/nx-sonarqube/src/executors/scan/utils/utils.ts b/packages/nx-sonarqube/src/executors/scan/utils/utils.ts index 68327ae..95118cb 100644 --- a/packages/nx-sonarqube/src/executors/scan/utils/utils.ts +++ b/packages/nx-sonarqube/src/executors/scan/utils/utils.ts @@ -1,5 +1,4 @@ import { ScanExecutorSchema } from '../schema'; - import { createProjectGraphAsync, DependencyType, @@ -14,12 +13,14 @@ import { tsquery } from '@phenomnomnominal/tsquery'; import { execSync } from 'child_process'; import * as sonarQubeScanner from 'sonarqube-scanner'; import { TargetConfiguration } from 'nx/src/config/workspace-json-project-json'; -import { readFileSync } from 'fs'; +import { readFileSync, existsSync } from 'fs'; interface OptionMarshaller { Options(): { [option: string]: string }; } +type Executor = '@nx/jest:jest' | '@nx/vite:test'; + export declare type WorkspaceLibrary = { name: string; type: DependencyType | string; @@ -48,6 +49,27 @@ class EnvMarshaller implements OptionMarshaller { } } +function getExecutor(executor: string): Executor { + if (executor === '@nx/vite:test') { + return '@nx/vite:test'; + } + + // Always fallback to the default executor: jest + return '@nx/jest:jest'; +} + +type CoverageDirectoryName = 'coverageDirectory' | 'reportsDirectory'; + +function getCoverageDirectoryName(executor: Executor): CoverageDirectoryName { + if (executor === '@nx/vite:test') { + return 'reportsDirectory'; + } + + // Always fallback to the default coverage directory for the default executor: jest + return 'coverageDirectory'; +} + + export async function determinePaths( options: ScanExecutorSchema, context: ExecutorContext @@ -71,18 +93,20 @@ export async function determinePaths( sources.push(dep.sourceRoot); if (dep.testTarget) { - if (dep.testTarget.options.coverageDirectory) { + const executor: Executor = getExecutor(dep.testTarget.executor); + const coverageDirectoryName: CoverageDirectoryName = getCoverageDirectoryName(executor); + + if (dep.testTarget.options?.[coverageDirectoryName]) { lcovPaths.push( joinPathFragments( - dep.testTarget.options.coverageDirectory + dep.testTarget.options[coverageDirectoryName] .replace(new RegExp(/'/g), '') .replace(/^(?:\.\.\/)+/, ''), 'lcov.info' ) ); - } else if (dep.testTarget.options.jestConfig) { + } else if (executor === '@nx/jest:jest' && dep.testTarget.options?.jestConfig) { const jestConfigPath = dep.testTarget.options.jestConfig; - const jestConfig = readFileSync(jestConfigPath, 'utf-8'); const ast = tsquery.ast(jestConfig); const nodes = tsquery( @@ -106,6 +130,40 @@ export async function determinePaths( `Skipping ${context.projectName} as it does not have a coverageDirectory in ${jestConfigPath}` ); } + } else if (executor === '@nx/vite:test') { + const configPath: string | undefined = getViteConfigPath(context.root, dep); + + if (configPath === undefined) { + logger.warn( + `Skipping ${context.projectName} as we cannot find a vite config file` + ); + + return; + } + + const config = readFileSync(configPath, 'utf-8'); + const ast = tsquery.ast(config); + const nodes = tsquery( + ast, + 'Identifier[name="reportsDirectory"] ~ StringLiteral', + { visitAllChildren: true } + ); + + if (nodes.length) { + lcovPaths.push( + joinPathFragments( + nodes[0] + .getText() + .replace(new RegExp(/'/g), '') + .replace(/^(?:\.\.\/)+/, ''), + 'lcov.info' + ) + ); + } else { + logger.warn( + `Skipping ${context.projectName} as it does not have a reportsDirectory in ${configPath}` + ); + } } else { logger.warn( `Skipping ${context.projectName} as it does not have a jestConfig` @@ -117,6 +175,7 @@ export async function determinePaths( ); } }); + return Promise.resolve({ lcovPaths: lcovPaths.join(','), sources: sources.join(','), @@ -147,6 +206,7 @@ export async function scanner( serverUrl: options.hostUrl, options: scannerOptions, }); + return { success: success, }; @@ -278,3 +338,25 @@ function collectDependencies( return dependencies; } + +export function normalizeViteConfigFilePathWithTree( + projectRoot: string, + configFilePath?: string +): string | undefined { + return configFilePath && existsSync(configFilePath) + ? configFilePath + : existsSync(joinPathFragments(`${projectRoot}/vite.config.ts`)) + ? joinPathFragments(`${projectRoot}/vite.config.ts`) + : existsSync(joinPathFragments(`${projectRoot}/vite.config.js`)) + ? joinPathFragments(`${projectRoot}/vite.config.js`) + : undefined; +} + +export function getViteConfigPath( + projectRoot: string, + dep: WorkspaceLibrary +): string | undefined { + const viteConfigPath: string | undefined = dep.testTarget.options?.configFile; + + return normalizeViteConfigFilePathWithTree(projectRoot, viteConfigPath); +}