From 574dd793c87f43832de8c0fb504155f40e5d91d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Thu, 18 Dec 2025 14:32:12 +0100 Subject: [PATCH] feat(plugin-typescript): log initializer and runner steps --- .../plugin-typescript/src/lib/constants.ts | 2 + packages/plugin-typescript/src/lib/format.ts | 4 ++ .../src/lib/runner/runner.int.test.ts | 4 +- .../src/lib/runner/runner.ts | 26 ++++++++-- .../src/lib/runner/runner.unit.test.ts | 50 +++++++++++-------- .../src/lib/runner/ts-runner.int.test.ts | 6 +-- .../src/lib/runner/ts-runner.ts | 17 +++++-- .../plugin-typescript/src/lib/runner/utils.ts | 16 ++---- .../src/lib/typescript-plugin.ts | 28 ++++++----- packages/plugin-typescript/src/lib/utils.ts | 40 ++++++++++++--- .../src/lib/utils.unit.test.ts | 34 +++++++------ 11 files changed, 147 insertions(+), 80 deletions(-) create mode 100644 packages/plugin-typescript/src/lib/format.ts diff --git a/packages/plugin-typescript/src/lib/constants.ts b/packages/plugin-typescript/src/lib/constants.ts index 25edb0924..7f018d91a 100644 --- a/packages/plugin-typescript/src/lib/constants.ts +++ b/packages/plugin-typescript/src/lib/constants.ts @@ -4,6 +4,8 @@ import { TS_CODE_RANGE_NAMES } from './runner/ts-error-codes.js'; import type { AuditSlug } from './types.js'; export const TYPESCRIPT_PLUGIN_SLUG = 'typescript'; +export const TYPESCRIPT_PLUGIN_TITLE = 'TypeScript'; + export const DEFAULT_TS_CONFIG = 'tsconfig.json'; const AUDIT_DESCRIPTIONS: Record = { diff --git a/packages/plugin-typescript/src/lib/format.ts b/packages/plugin-typescript/src/lib/format.ts new file mode 100644 index 000000000..76f37d13f --- /dev/null +++ b/packages/plugin-typescript/src/lib/format.ts @@ -0,0 +1,4 @@ +import { pluginMetaLogFormatter } from '@code-pushup/utils'; +import { TYPESCRIPT_PLUGIN_TITLE } from './constants.js'; + +export const formatMetaLog = pluginMetaLogFormatter(TYPESCRIPT_PLUGIN_TITLE); 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 eb4e41683..a7219d813 100644 --- a/packages/plugin-typescript/src/lib/runner/runner.int.test.ts +++ b/packages/plugin-typescript/src/lib/runner/runner.int.test.ts @@ -1,5 +1,5 @@ import { describe, expect } from 'vitest'; -import type { AuditOutputs } from '@code-pushup/models'; +import { type AuditOutputs, DEFAULT_PERSIST_CONFIG } from '@code-pushup/models'; import { osAgnosticAuditOutputs } from '@code-pushup/test-utils'; import { getAudits } from '../utils.js'; import { createRunnerFunction } from './runner.js'; @@ -12,7 +12,7 @@ describe('createRunnerFunction', () => { expectedAudits: getAudits(), }); - const result = await runnerFunction(); + const result = await runnerFunction({ persist: DEFAULT_PERSIST_CONFIG }); expect(osAgnosticAuditOutputs(result as AuditOutputs)).toMatchSnapshot(); }, 35_000); diff --git a/packages/plugin-typescript/src/lib/runner/runner.ts b/packages/plugin-typescript/src/lib/runner/runner.ts index 0b77b4d98..2da4c1a27 100644 --- a/packages/plugin-typescript/src/lib/runner/runner.ts +++ b/packages/plugin-typescript/src/lib/runner/runner.ts @@ -4,7 +4,12 @@ import type { Issue, RunnerFunction, } from '@code-pushup/models'; -import { pluralizeToken } from '@code-pushup/utils'; +import { + formatAsciiTable, + logger, + pluralizeToken, + toSentenceCase, +} from '@code-pushup/utils'; import type { AuditSlug } from '../types.js'; import { type DiagnosticsOptions, @@ -19,8 +24,10 @@ export type RunnerOptions = DiagnosticsOptions & { export function createRunnerFunction(options: RunnerOptions): RunnerFunction { const { tsconfig, expectedAudits } = options; - return async (): Promise => { - const diagnostics = await getTypeScriptDiagnostics({ tsconfig }); + + return (): AuditOutputs => { + const diagnostics = getTypeScriptDiagnostics({ tsconfig }); + const result = diagnostics.reduce< Partial>> >((acc, diag) => { @@ -37,6 +44,19 @@ export function createRunnerFunction(options: RunnerOptions): RunnerFunction { }; }, {}); + logger.debug( + formatAsciiTable( + { + columns: ['left', 'right'], + rows: Object.values(result).map(audit => [ + `• ${toSentenceCase(audit.slug)}`, + audit.details?.issues?.length ?? 0, + ]), + }, + { borderless: true }, + ), + ); + return expectedAudits.map(({ slug }): AuditOutput => { const { details } = result[slug] ?? {}; diff --git a/packages/plugin-typescript/src/lib/runner/runner.unit.test.ts b/packages/plugin-typescript/src/lib/runner/runner.unit.test.ts index 58776c291..79d1bbb3c 100644 --- a/packages/plugin-typescript/src/lib/runner/runner.unit.test.ts +++ b/packages/plugin-typescript/src/lib/runner/runner.unit.test.ts @@ -4,7 +4,11 @@ import { type SourceFile, } from 'typescript'; import { beforeEach, describe, expect } from 'vitest'; -import { auditOutputsSchema } from '@code-pushup/models'; +import { + DEFAULT_PERSIST_CONFIG, + type RunnerArgs, + auditOutputsSchema, +} from '@code-pushup/models'; import { createRunnerFunction } from './runner.js'; import * as runnerModule from './ts-runner.js'; import * as utilsModule from './utils.js'; @@ -20,6 +24,8 @@ describe('createRunnerFunction', () => { 'getIssueFromDiagnostic', ); + const runnerArgs: RunnerArgs = { persist: DEFAULT_PERSIST_CONFIG }; + const semanticTsCode = 2322; const mockSemanticDiagnostic = { code: semanticTsCode, // "Type 'string' is not assignable to type 'number'" @@ -47,51 +53,51 @@ describe('createRunnerFunction', () => { getTypeScriptDiagnosticsSpy.mockReset(); }); - it('should return empty array if no diagnostics are found', async () => { - getTypeScriptDiagnosticsSpy.mockResolvedValue([]); + it('should return empty array if no diagnostics are found', () => { + getTypeScriptDiagnosticsSpy.mockReturnValue([]); const runner = createRunnerFunction({ tsconfig: 'tsconfig.json', expectedAudits: [], }); - await expect(runner(() => void 0)).resolves.toStrictEqual([]); + expect(runner(runnerArgs)).toStrictEqual([]); }); - it('should return empty array if no supported diagnostics are found', async () => { - getTypeScriptDiagnosticsSpy.mockResolvedValue([mockSemanticDiagnostic]); + it('should return empty array if no supported diagnostics are found', () => { + getTypeScriptDiagnosticsSpy.mockReturnValue([mockSemanticDiagnostic]); const runner = createRunnerFunction({ tsconfig: 'tsconfig.json', expectedAudits: [], }); - await expect(runner(() => void 0)).resolves.toStrictEqual([]); + expect(runner(runnerArgs)).toStrictEqual([]); }); - it('should pass the diagnostic code to tsCodeToSlug', async () => { - getTypeScriptDiagnosticsSpy.mockResolvedValue([mockSemanticDiagnostic]); + it('should pass the diagnostic code to tsCodeToSlug', () => { + getTypeScriptDiagnosticsSpy.mockReturnValue([mockSemanticDiagnostic]); const runner = createRunnerFunction({ tsconfig: 'tsconfig.json', expectedAudits: [], }); - await expect(runner(() => void 0)).resolves.toStrictEqual([]); + expect(runner(runnerArgs)).toStrictEqual([]); expect(tSCodeToAuditSlugSpy).toHaveBeenCalledTimes(1); expect(tSCodeToAuditSlugSpy).toHaveBeenCalledWith(semanticTsCode); }); - it('should pass the diagnostic to getIssueFromDiagnostic', async () => { - getTypeScriptDiagnosticsSpy.mockResolvedValue([mockSemanticDiagnostic]); + it('should pass the diagnostic to getIssueFromDiagnostic', () => { + getTypeScriptDiagnosticsSpy.mockReturnValue([mockSemanticDiagnostic]); const runner = createRunnerFunction({ tsconfig: 'tsconfig.json', expectedAudits: [], }); - await expect(runner(() => void 0)).resolves.toStrictEqual([]); + expect(runner(runnerArgs)).toStrictEqual([]); expect(getIssueFromDiagnosticSpy).toHaveBeenCalledTimes(1); expect(getIssueFromDiagnosticSpy).toHaveBeenCalledWith( mockSemanticDiagnostic, ); }); - it('should return multiple issues per audit', async () => { + it('should return multiple issues per audit', () => { const code = 2222; - getTypeScriptDiagnosticsSpy.mockResolvedValue([ + getTypeScriptDiagnosticsSpy.mockReturnValue([ mockSemanticDiagnostic, { ...mockSemanticDiagnostic, @@ -104,7 +110,7 @@ describe('createRunnerFunction', () => { expectedAudits: [{ slug: 'semantic-errors' }], }); - const auditOutputs = await runner(() => void 0); + const auditOutputs = runner(runnerArgs); expect(auditOutputs).toStrictEqual([ { slug: 'semantic-errors', @@ -126,8 +132,8 @@ describe('createRunnerFunction', () => { expect(() => auditOutputsSchema.parse(auditOutputs)).not.toThrow(); }); - it('should return multiple audits', async () => { - getTypeScriptDiagnosticsSpy.mockResolvedValue([ + it('should return multiple audits', () => { + getTypeScriptDiagnosticsSpy.mockReturnValue([ mockSyntacticDiagnostic, mockSemanticDiagnostic, ]); @@ -136,7 +142,7 @@ describe('createRunnerFunction', () => { expectedAudits: [{ slug: 'semantic-errors' }, { slug: 'syntax-errors' }], }); - const auditOutputs = await runner(() => void 0); + const auditOutputs = runner(runnerArgs); expect(auditOutputs).toStrictEqual([ expect.objectContaining({ slug: 'semantic-errors', @@ -159,8 +165,8 @@ describe('createRunnerFunction', () => { ]); }); - it('should return valid AuditOutput shape', async () => { - getTypeScriptDiagnosticsSpy.mockResolvedValue([ + it('should return valid AuditOutput shape', () => { + getTypeScriptDiagnosticsSpy.mockReturnValue([ mockSyntacticDiagnostic, { ...mockSyntacticDiagnostic, @@ -178,7 +184,7 @@ describe('createRunnerFunction', () => { tsconfig: 'tsconfig.json', expectedAudits: [{ slug: 'semantic-errors' }, { slug: 'syntax-errors' }], }); - const auditOutputs = await runner(() => void 0); + const auditOutputs = runner(runnerArgs); expect(() => auditOutputsSchema.parse(auditOutputs)).not.toThrow(); }); }); diff --git a/packages/plugin-typescript/src/lib/runner/ts-runner.int.test.ts b/packages/plugin-typescript/src/lib/runner/ts-runner.int.test.ts index 7ec54c910..2f6dbd107 100644 --- a/packages/plugin-typescript/src/lib/runner/ts-runner.int.test.ts +++ b/packages/plugin-typescript/src/lib/runner/ts-runner.int.test.ts @@ -2,12 +2,12 @@ import { describe, expect } from 'vitest'; import { getTypeScriptDiagnostics } from './ts-runner.js'; describe('getTypeScriptDiagnostics', () => { - it('should return valid diagnostics', async () => { - await expect( + it('should return valid diagnostics', () => { + expect( getTypeScriptDiagnostics({ tsconfig: 'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.json', }), - ).resolves.toHaveLength(8); + ).toHaveLength(8); }); }); diff --git a/packages/plugin-typescript/src/lib/runner/ts-runner.ts b/packages/plugin-typescript/src/lib/runner/ts-runner.ts index 46e9ea9df..6103b6e1e 100644 --- a/packages/plugin-typescript/src/lib/runner/ts-runner.ts +++ b/packages/plugin-typescript/src/lib/runner/ts-runner.ts @@ -3,23 +3,30 @@ import { createProgram, getPreEmitDiagnostics, } from 'typescript'; -import { stringifyError } from '@code-pushup/utils'; +import { logger, pluralizeToken, stringifyError } from '@code-pushup/utils'; import { loadTargetConfig } from './utils.js'; export type DiagnosticsOptions = { tsconfig: string; }; -export async function getTypeScriptDiagnostics({ +export function getTypeScriptDiagnostics({ tsconfig, -}: DiagnosticsOptions): Promise { +}: DiagnosticsOptions): readonly Diagnostic[] { const { fileNames, options } = loadTargetConfig(tsconfig); + logger.info( + `Parsed TypeScript config file ${tsconfig}, program includes ${pluralizeToken('file', fileNames.length)}`, + ); try { const program = createProgram(fileNames, options); - return getPreEmitDiagnostics(program); + const diagnostics = getPreEmitDiagnostics(program); + logger.info( + `TypeScript compiler found ${pluralizeToken('diagnostic', diagnostics.length)}`, + ); + return diagnostics; } catch (error) { throw new Error( - `Can't create TS program in getDiagnostics. \n ${stringifyError(error)}`, + `Can't create TS program and get diagnostics.\n${stringifyError(error)}`, ); } } diff --git a/packages/plugin-typescript/src/lib/runner/utils.ts b/packages/plugin-typescript/src/lib/runner/utils.ts index 52dfed704..23c3ee67b 100644 --- a/packages/plugin-typescript/src/lib/runner/utils.ts +++ b/packages/plugin-typescript/src/lib/runner/utils.ts @@ -14,7 +14,7 @@ import type { CodeRangeName } from './types.js'; /** * Transform the TypeScript error code to the audit slug. - * @param code - The TypeScript error code. + * @param code The TypeScript error code. * @returns The audit slug. * @throws Error if the code is not supported. */ @@ -31,7 +31,7 @@ export function tsCodeToAuditSlug(code: number): CodeRangeName { * - ts.DiagnosticCategory.Error (2) * - ts.DiagnosticCategory.Suggestion (3) * - ts.DiagnosticCategory.Message (4) - * @param category - The TypeScript diagnostic category. + * @param category The TypeScript diagnostic category. * @returns The severity of the issue. */ export function getSeverity(category: DiagnosticCategory): Issue['severity'] { @@ -47,7 +47,7 @@ export function getSeverity(category: DiagnosticCategory): Issue['severity'] { /** * Format issue message from the TypeScript diagnostic. - * @param diag - The TypeScript diagnostic. + * @param diag The TypeScript diagnostic. * @returns The issue message. */ export function getMessage(diag: Diagnostic): string { @@ -60,7 +60,7 @@ export function getMessage(diag: Diagnostic): string { /** * Get the issue from the TypeScript diagnostic. - * @param diag - The TypeScript diagnostic. + * @param diag The TypeScript diagnostic. * @returns The issue. * @throws Error if the diagnostic is global (e.g., invalid compiler option). */ @@ -84,13 +84,7 @@ export function getIssueFromDiagnostic(diag: Diagnostic) { ...issue, source: { file: path.relative(process.cwd(), diag.file.fileName), - ...(startLine - ? { - position: { - startLine, - }, - } - : {}), + ...(startLine ? { position: { startLine } } : {}), }, } satisfies Issue; } diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.ts b/packages/plugin-typescript/src/lib/typescript-plugin.ts index 0f2fc85cc..3334fb2e7 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.ts @@ -1,14 +1,18 @@ import { createRequire } from 'node:module'; import { type PluginConfig, validate } from '@code-pushup/models'; import { stringifyError } from '@code-pushup/utils'; -import { DEFAULT_TS_CONFIG, TYPESCRIPT_PLUGIN_SLUG } from './constants.js'; +import { + DEFAULT_TS_CONFIG, + TYPESCRIPT_PLUGIN_SLUG, + TYPESCRIPT_PLUGIN_TITLE, +} from './constants.js'; import { createRunnerFunction } from './runner/runner.js'; import { type TypescriptPluginConfig, type TypescriptPluginOptions, typescriptPluginConfigSchema, } from './schema.js'; -import { getAudits, getGroups, logSkippedAudits } from './utils.js'; +import { getAudits, getGroups, logAuditsAndGroups } from './utils.js'; const packageJson = createRequire(import.meta.url)( '../../package.json', @@ -23,24 +27,24 @@ export function typescriptPlugin( scoreTargets, } = parseOptions(options ?? {}); - const filteredAudits = getAudits({ onlyAudits }); - const filteredGroups = getGroups({ onlyAudits }); + const audits = getAudits({ onlyAudits }); + const groups = getGroups({ onlyAudits }); - logSkippedAudits(filteredAudits); + logAuditsAndGroups(audits, groups); return { slug: TYPESCRIPT_PLUGIN_SLUG, - packageName: packageJson.name, - version: packageJson.version, - title: 'TypeScript', + title: TYPESCRIPT_PLUGIN_TITLE, + icon: 'typescript', description: 'Official Code PushUp TypeScript plugin.', docsUrl: 'https://www.npmjs.com/package/@code-pushup/typescript-plugin/', - icon: 'typescript', - audits: filteredAudits, - groups: filteredGroups, + packageName: packageJson.name, + version: packageJson.version, + audits, + groups, runner: createRunnerFunction({ tsconfig, - expectedAudits: filteredAudits, + expectedAudits: audits, }), ...(scoreTargets && { scoreTargets }), }; diff --git a/packages/plugin-typescript/src/lib/utils.ts b/packages/plugin-typescript/src/lib/utils.ts index e4eee7837..39ca76e53 100644 --- a/packages/plugin-typescript/src/lib/utils.ts +++ b/packages/plugin-typescript/src/lib/utils.ts @@ -1,7 +1,17 @@ import type { CompilerOptions } from 'typescript'; -import type { Audit, CategoryConfig, CategoryRef } from '@code-pushup/models'; -import { kebabCaseToCamelCase, logger } from '@code-pushup/utils'; +import type { + Audit, + CategoryConfig, + CategoryRef, + Group, +} from '@code-pushup/models'; +import { + kebabCaseToCamelCase, + logger, + pluralizeToken, +} from '@code-pushup/utils'; import { AUDITS, GROUPS, TYPESCRIPT_PLUGIN_SLUG } from './constants.js'; +import { formatMetaLog } from './format.js'; import type { TypescriptPluginConfig, TypescriptPluginOptions, @@ -141,11 +151,29 @@ export function getCategories() { return Object.values(CATEGORY_MAP); } -export function logSkippedAudits(audits: Audit[]) { +export function logAuditsAndGroups(audits: Audit[], groups: Group[]) { + logger.info( + formatMetaLog( + `Created ${pluralizeToken('audit', audits.length)} and ${pluralizeToken('group', groups.length)}`, + ), + ); const skippedAudits = AUDITS.filter( - audit => !audits.some(filtered => filtered.slug === audit.slug), - ).map(audit => kebabCaseToCamelCase(audit.slug)); + audit => !audits.some(({ slug }) => slug === audit.slug), + ); + const skippedGroups = GROUPS.filter( + group => !groups.some(({ slug }) => slug === group.slug), + ); if (skippedAudits.length > 0) { - logger.info(`Skipped audits: [${skippedAudits.join(', ')}]`); + logger.info( + formatMetaLog( + [ + `Skipped ${pluralizeToken('audit', skippedAudits.length)}`, + skippedGroups.length > 0 && + pluralizeToken('group', skippedGroups.length), + ] + .filter(Boolean) + .join(' and '), + ), + ); } } diff --git a/packages/plugin-typescript/src/lib/utils.unit.test.ts b/packages/plugin-typescript/src/lib/utils.unit.test.ts index f9fd9b99e..0acc22ba9 100644 --- a/packages/plugin-typescript/src/lib/utils.unit.test.ts +++ b/packages/plugin-typescript/src/lib/utils.unit.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from 'vitest'; import { type Audit, categoryRefSchema } from '@code-pushup/models'; import { logger } from '@code-pushup/utils'; -import { AUDITS } from './constants.js'; +import { AUDITS, GROUPS } from './constants.js'; import { filterAuditsByCompilerOptions, filterAuditsBySlug, getCategoryRefsFromGroups, - logSkippedAudits, + logAuditsAndGroups, } from './utils.js'; describe('filterAuditsBySlug', () => { @@ -114,26 +114,28 @@ describe('getCategoryRefsFromGroups', () => { }); }); -describe('logSkippedAudits', () => { - it('should not print anything when all audits are included', () => { - logSkippedAudits(AUDITS); - - expect(logger.info).not.toHaveBeenCalled(); - }); - - it('should warn about skipped audits', () => { - logSkippedAudits(AUDITS.slice(0, -1)); +describe('logAuditsAndGroups', () => { + it('should log only once if nothing was skipped', () => { + logAuditsAndGroups(AUDITS, GROUPS); + expect(logger.info).toHaveBeenCalledTimes(1); expect(logger.info).toHaveBeenCalledWith( - expect.stringContaining(`Skipped audits: [`), + expect.stringMatching(/Created \d+ audits and \d+ groups$/), ); }); - it('should camel case the slugs in the audit message', () => { - logSkippedAudits(AUDITS.slice(0, -1)); + it('should log skipped audits and groups', () => { + const groups = GROUPS.slice(0, 1); + const audits = AUDITS.filter(audit => + groups[0]!.refs.some(ref => ref.slug === audit.slug), + ); - expect(logger.info).toHaveBeenCalledWith( - expect.stringContaining(`unknownCodes`), + logAuditsAndGroups(audits, groups); + + expect(logger.info).toHaveBeenCalledTimes(2); + expect(logger.info).toHaveBeenNthCalledWith( + 2, + expect.stringMatching(/Skipped \d+ audits and \d+ groups$/), ); }); });