diff --git a/code-pushup.config.ts b/code-pushup.config.ts index 51aa2b33b..b9234bacd 100644 --- a/code-pushup.config.ts +++ b/code-pushup.config.ts @@ -26,7 +26,7 @@ const config: CoreConfig = { server: env.CP_SERVER, apiKey: env.CP_API_KEY, organization: env.CP_ORGANIZATION, - project: env.CP_PROJECT, + project: 'cli-workspace', }, }), diff --git a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/tsconfig.json b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/tsconfig.json index b9dd367e9..400df8816 100644 --- a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/tsconfig.json +++ b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "rootDir": "./src", + "rootDir": "${configDir}/src", "target": "ES6", "module": "CommonJS", "strict": true, diff --git a/e2e/plugin-typescript-e2e/tests/__snapshots__/collect.e2e.test.ts.snap b/e2e/plugin-typescript-e2e/tests/__snapshots__/collect.e2e.test.ts.snap index 09799e984..adb471f4a 100644 --- a/e2e/plugin-typescript-e2e/tests/__snapshots__/collect.e2e.test.ts.snap +++ b/e2e/plugin-typescript-e2e/tests/__snapshots__/collect.e2e.test.ts.snap @@ -121,7 +121,7 @@ exports[`PLUGIN collect report with typescript-plugin NPM package > should run p "details": { "issues": [ { - "message": "TS6059: File './exclude/utils.ts' is not under 'rootDir' 'src'. 'rootDir' is expected to contain all source files.", + "message": "TS6059: File './exclude/utils.ts' is not under 'rootDir' './src'. 'rootDir' is expected to contain all source files.", "severity": "error", "source": { "file": "tmp/e2e/plugin-typescript-e2e/src/6-configuration-errors.ts", diff --git a/e2e/plugin-typescript-e2e/tests/collect.e2e.test.ts b/e2e/plugin-typescript-e2e/tests/collect.e2e.test.ts index 648d56a07..1d3766c37 100644 --- a/e2e/plugin-typescript-e2e/tests/collect.e2e.test.ts +++ b/e2e/plugin-typescript-e2e/tests/collect.e2e.test.ts @@ -7,10 +7,35 @@ import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, omitVariableReportData, + osAgnosticAuditOutputs, + osAgnosticPath, teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess, readJsonFile } from '@code-pushup/utils'; +function sanitizeReportPaths(report: Report): Report { + return { + ...report, + plugins: report.plugins.map(plugin => ({ + ...plugin, + audits: osAgnosticAuditOutputs(plugin.audits, message => + message.replace( + /['"]([^'"]*[/\\][^'"]*)['"]/g, + (fullMatch: string, capturedPath: string) => { + const osAgnostic = osAgnosticPath(capturedPath); + // Only replace directory paths, not .ts file paths + if (capturedPath.endsWith('.ts')) { + return `'${osAgnostic}'`; + } + // on Windows the path starts from "plugin-typescript-e2e/src" not "./". This normalizes it to "./" + return `'${['.', osAgnostic.split('/').slice(-1)].join('/')}'`; + }, + ), + ), + })), + }; +} + describe('PLUGIN collect report with typescript-plugin NPM package', () => { const envRoot = path.join(E2E_ENVIRONMENTS_DIR, nxTargetProject()); const distRoot = path.join(envRoot, TEST_OUTPUT_DIR); @@ -57,6 +82,9 @@ describe('PLUGIN collect report with typescript-plugin NPM package', () => { path.join(envRoot, outputDir, 'report.json'), ); expect(() => reportSchema.parse(reportJson)).not.toThrow(); - expect(omitVariableReportData(reportJson)).toMatchSnapshot(); + + expect( + omitVariableReportData(sanitizeReportPaths(reportJson)), + ).toMatchSnapshot(); }); }); diff --git a/packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.extends-base.json b/packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.extends-base.json new file mode 100644 index 000000000..d98091ca6 --- /dev/null +++ b/packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.extends-base.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "rootDir": "${configDir}", + "verbatimModuleSyntax": false + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.extends-extending.json b/packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.extends-extending.json new file mode 100644 index 000000000..f1b970c13 --- /dev/null +++ b/packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.extends-extending.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.extends-base.json", + "compilerOptions": { + "verbatimModuleSyntax": true, + "module": "CommonJS" + }, + "exclude": ["src/*-errors/**/*.ts"] +} diff --git a/packages/plugin-typescript/src/lib/runner/runner.int.test.ts b/packages/plugin-typescript/src/lib/runner/runner.int.test.ts index d68fc82bb..eb4e41683 100644 --- a/packages/plugin-typescript/src/lib/runner/runner.int.test.ts +++ b/packages/plugin-typescript/src/lib/runner/runner.int.test.ts @@ -1,15 +1,19 @@ import { describe, expect } from 'vitest'; +import type { AuditOutputs } from '@code-pushup/models'; +import { osAgnosticAuditOutputs } from '@code-pushup/test-utils'; import { getAudits } from '../utils.js'; import { createRunnerFunction } from './runner.js'; describe('createRunnerFunction', () => { it('should create valid audit outputs when called', async () => { - await expect( - createRunnerFunction({ - tsconfig: - 'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.all-audits.json', - expectedAudits: getAudits(), - })(() => void 0), - ).resolves.toMatchSnapshot(); + const runnerFunction = createRunnerFunction({ + tsconfig: + 'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.all-audits.json', + expectedAudits: getAudits(), + }); + + const result = await runnerFunction(); + + expect(osAgnosticAuditOutputs(result as AuditOutputs)).toMatchSnapshot(); }, 35_000); }); diff --git a/packages/plugin-typescript/src/lib/runner/ts-runner.ts b/packages/plugin-typescript/src/lib/runner/ts-runner.ts index 147d15e72..46e9ea9df 100644 --- a/packages/plugin-typescript/src/lib/runner/ts-runner.ts +++ b/packages/plugin-typescript/src/lib/runner/ts-runner.ts @@ -13,7 +13,7 @@ export type DiagnosticsOptions = { export async function getTypeScriptDiagnostics({ tsconfig, }: DiagnosticsOptions): Promise { - const { fileNames, options } = await loadTargetConfig(tsconfig); + const { fileNames, options } = loadTargetConfig(tsconfig); try { const program = createProgram(fileNames, options); return getPreEmitDiagnostics(program); diff --git a/packages/plugin-typescript/src/lib/runner/utils.int.test.ts b/packages/plugin-typescript/src/lib/runner/utils.int.test.ts index 63b1f0a2f..c202d59be 100644 --- a/packages/plugin-typescript/src/lib/runner/utils.int.test.ts +++ b/packages/plugin-typescript/src/lib/runner/utils.int.test.ts @@ -1,28 +1,28 @@ import * as tsModule from 'typescript'; -import { describe, expect } from 'vitest'; +import { describe, expect, vi } from 'vitest'; +import { osAgnosticPath } from '@code-pushup/test-utils'; import { loadTargetConfig } from './utils.js'; describe('loadTargetConfig', () => { - const parseConfigFileTextToJsonSpy = vi.spyOn( - tsModule, - 'parseConfigFileTextToJson', - ); + const readConfigFileSpy = vi.spyOn(tsModule, 'readConfigFile'); const parseJsonConfigFileContentSpy = vi.spyOn( tsModule, 'parseJsonConfigFileContent', ); - it('should return the parsed content of a tsconfig file and ist TypeScript helper to parse it', async () => { - await expect( + it('should return the parsed content of a tsconfig file and ist TypeScript helper to parse it', () => { + expect( loadTargetConfig( - 'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.init.json', + osAgnosticPath( + 'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.init.json', + ), ), - ).resolves.toStrictEqual( + ).toStrictEqual( expect.objectContaining({ fileNames: expect.any(Array), options: { module: 1, - configFilePath: undefined, + configFilePath: expect.stringContaining('tsconfig.init.json'), esModuleInterop: true, forceConsistentCasingInFileNames: true, skipLibCheck: true, @@ -31,25 +31,39 @@ describe('loadTargetConfig', () => { }, }), ); - expect(parseConfigFileTextToJsonSpy).toHaveBeenCalledTimes(1); - expect(parseConfigFileTextToJsonSpy).toHaveBeenCalledWith( - 'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.init.json', - expect.stringContaining('/* Projects */'), + expect(readConfigFileSpy).toHaveBeenCalledTimes(1); + expect(readConfigFileSpy).toHaveBeenCalledWith( + expect.stringContaining('tsconfig.init.json'), + expect.any(Function), ); expect(parseJsonConfigFileContentSpy).toHaveBeenCalledTimes(1); - expect(parseJsonConfigFileContentSpy).toHaveBeenCalledWith( + }); + + it('should return the parsed content of a tsconfig file that extends another config', () => { + expect( + loadTargetConfig( + 'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.extends-extending.json', + ), + ).toStrictEqual( expect.objectContaining({ - compilerOptions: expect.objectContaining({ - esModuleInterop: true, - forceConsistentCasingInFileNames: true, - module: 'commonjs', - skipLibCheck: true, - strict: true, - target: 'es2016', + fileNames: expect.arrayContaining([ + // from tsconfig.extends-base.json#includes and tsconfig.extends-extending.json#excludes + expect.stringMatching(/src[/\\]0-no-diagnostics[/\\]/), + ]), + options: expect.objectContaining({ + // Options from tsconfig.extends-base.json + rootDir: expect.stringMatching(/basic-setup$/), + // Options from tsconfig.extends-extending.json + module: 1, + configFilePath: expect.stringContaining( + 'tsconfig.extends-extending.json', + ), + verbatimModuleSyntax: true, // Overrides base config's false }), }), - expect.any(Object), - expect.any(String), ); + + expect(readConfigFileSpy).toHaveBeenCalledTimes(1); + expect(parseJsonConfigFileContentSpy).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/plugin-typescript/src/lib/runner/utils.ts b/packages/plugin-typescript/src/lib/runner/utils.ts index da87f2327..52dfed704 100644 --- a/packages/plugin-typescript/src/lib/runner/utils.ts +++ b/packages/plugin-typescript/src/lib/runner/utils.ts @@ -1,15 +1,14 @@ -// eslint-disable-next-line unicorn/import-style -import { dirname } from 'node:path'; +import path from 'node:path'; import { type Diagnostic, DiagnosticCategory, flattenDiagnosticMessageText, - parseConfigFileTextToJson, parseJsonConfigFileContent, + readConfigFile, sys, } from 'typescript'; import type { Issue } from '@code-pushup/models'; -import { readTextFile, truncateIssueMessage } from '@code-pushup/utils'; +import { truncateIssueMessage } from '@code-pushup/utils'; import { TS_CODE_RANGE_NAMES } from './ts-error-codes.js'; import type { CodeRangeName } from './types.js'; @@ -84,7 +83,7 @@ export function getIssueFromDiagnostic(diag: Diagnostic) { return { ...issue, source: { - file: diag.file.fileName, + file: path.relative(process.cwd(), diag.file.fileName), ...(startLine ? { position: { @@ -96,16 +95,22 @@ export function getIssueFromDiagnostic(diag: Diagnostic) { } satisfies Issue; } -export async function loadTargetConfig(tsConfigPath: string) { - const { config } = parseConfigFileTextToJson( - tsConfigPath, - await readTextFile(tsConfigPath), - ); +export function loadTargetConfig(tsConfigPath: string) { + const resolvedConfigPath = path.resolve(tsConfigPath); + const { config, error } = readConfigFile(resolvedConfigPath, sys.readFile); + + if (error) { + throw new Error( + `Error reading TypeScript config file at ${tsConfigPath}:\n${error.messageText}`, + ); + } const parsedConfig = parseJsonConfigFileContent( config, sys, - dirname(tsConfigPath), + path.dirname(resolvedConfigPath), + {}, + resolvedConfigPath, ); if (parsedConfig.fileNames.length === 0) { diff --git a/packages/plugin-typescript/vitest.int.config.ts b/packages/plugin-typescript/vitest.int.config.ts index 42d047592..87ad2157a 100644 --- a/packages/plugin-typescript/vitest.int.config.ts +++ b/packages/plugin-typescript/vitest.int.config.ts @@ -16,7 +16,12 @@ export default defineConfig({ coverage: { reporter: ['text', 'lcov'], reportsDirectory: '../../coverage/plugin-typescript/int-tests', - exclude: ['mocks/**', '**/types.ts'], + exclude: [ + 'mocks/**', + '**/types.ts', + '**/index.ts', + 'vitest.{unit,int}.config.ts', + ], }, environment: 'node', include: ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], diff --git a/packages/plugin-typescript/vitest.unit.config.ts b/packages/plugin-typescript/vitest.unit.config.ts index f4b1337f9..7046393e3 100644 --- a/packages/plugin-typescript/vitest.unit.config.ts +++ b/packages/plugin-typescript/vitest.unit.config.ts @@ -16,7 +16,12 @@ export default defineConfig({ coverage: { reporter: ['text', 'lcov'], reportsDirectory: '../../coverage/plugin-typescript/unit-tests', - exclude: ['mocks/**', '**/types.ts'], + exclude: [ + 'mocks/**', + '**/types.ts', + '**/index.ts', + 'vitest.{unit,int}.config.ts', + ], }, environment: 'node', include: ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], diff --git a/project.json b/project.json index a4c550bfb..1f19a828e 100644 --- a/project.json +++ b/project.json @@ -1,5 +1,5 @@ { - "name": "@code-pushup/cli-source", + "name": "cli-workspace", "$schema": "node_modules/nx/schemas/project-schema.json", "targets": { "code-pushup": { diff --git a/testing/test-utils/src/lib/utils/os-agnostic-paths.ts b/testing/test-utils/src/lib/utils/os-agnostic-paths.ts index 4c2def392..9dd00fb5e 100644 --- a/testing/test-utils/src/lib/utils/os-agnostic-paths.ts +++ b/testing/test-utils/src/lib/utils/os-agnostic-paths.ts @@ -1,8 +1,4 @@ -import type { - AuditOutput, - AuditOutputs, - AuditReport, -} from '@code-pushup/models'; +import type { AuditOutput, AuditReport } from '@code-pushup/models'; const AGNOSTIC_PATH_SEP_REGEX = /[/\\]/g; const OS_AGNOSTIC_PATH_SEP = '/'; @@ -102,6 +98,7 @@ export function osAgnosticPath(filePath?: string): string | undefined { export function osAgnosticAudit( audit: T, + transformMessage: (message: string) => string = s => s, ): T { const { issues = [] } = audit.details ?? {}; if (issues.every(({ source }) => source == null)) { @@ -119,12 +116,18 @@ export function osAgnosticAudit( ...issue.source, file: osAgnosticPath(issue.source.file), }, + message: transformMessage(issue.message), }, ), }, }; } -export function osAgnosticAuditOutputs(audits: AuditOutputs): AuditOutputs { - return audits.map(osAgnosticAudit); +export function osAgnosticAuditOutputs( + audits: T[], + transformAuditIssueMessage?: (message: string) => string, +): T[] { + return audits.map(audit => + osAgnosticAudit(audit, transformAuditIssueMessage), + ); }