From a81c8c3dce904b6bc1232400f8b1325460881fc7 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 Feb 2025 16:30:08 +0100 Subject: [PATCH 01/13] feat(plugin-typescript): add plugin logic --- packages/plugin-typescript/src/lib/schema.ts | 24 ++++++ .../src/lib/schema.unit.test.ts | 72 +++++++++++++++++ .../src/lib/typescript-plugin.ts | 57 +++++++++++++ .../src/lib/typescript-plugin.unit.test.ts | 31 +++++++ packages/utils/src/lib/string.ts | 58 +++++++++++++ packages/utils/src/lib/string.unit.test.ts | 81 +++++++++++++++++++ 6 files changed, 323 insertions(+) create mode 100644 packages/plugin-typescript/src/lib/schema.ts create mode 100644 packages/plugin-typescript/src/lib/schema.unit.test.ts create mode 100644 packages/plugin-typescript/src/lib/typescript-plugin.ts create mode 100644 packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts create mode 100644 packages/utils/src/lib/string.ts create mode 100644 packages/utils/src/lib/string.unit.test.ts diff --git a/packages/plugin-typescript/src/lib/schema.ts b/packages/plugin-typescript/src/lib/schema.ts new file mode 100644 index 000000000..54270bcc2 --- /dev/null +++ b/packages/plugin-typescript/src/lib/schema.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; +import { AUDITS, DEFAULT_TS_CONFIG } from './constants.js'; +import type { AuditSlug } from './types.js'; + +const auditSlugs = AUDITS.map(({ slug }) => slug) as [ + AuditSlug, + ...AuditSlug[], +]; +export const typescriptPluginConfigSchema = z.object({ + tsconfig: z + .string({ + description: 'Path to the TsConfig', + }) + .default(DEFAULT_TS_CONFIG), + onlyAudits: z + .array(z.enum(auditSlugs), { + description: 'Array with specific TsCodes to measure', + }) + .optional(), +}); + +export type TypescriptPluginOptions = z.infer< + typeof typescriptPluginConfigSchema +>; diff --git a/packages/plugin-typescript/src/lib/schema.unit.test.ts b/packages/plugin-typescript/src/lib/schema.unit.test.ts new file mode 100644 index 000000000..fd09145f6 --- /dev/null +++ b/packages/plugin-typescript/src/lib/schema.unit.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest'; +import { + type TypescriptPluginOptions, + typescriptPluginConfigSchema, +} from './schema.js'; + +describe('typescriptPluginConfigSchema', () => { + const tsConfigPath = 'tsconfig.json'; + + it('accepts a empty configuration', () => { + expect(() => typescriptPluginConfigSchema.parse({})).not.toThrow(); + }); + + it('accepts a configuration with tsConfigPath set', () => { + expect(() => + typescriptPluginConfigSchema.parse({ + tsConfigPath, + } satisfies TypescriptPluginOptions), + ).not.toThrow(); + }); + + it('accepts a configuration with tsConfigPath and empty onlyAudits', () => { + expect(() => + typescriptPluginConfigSchema.parse({ + tsConfigPath, + onlyAudits: [], + } satisfies TypescriptPluginOptions), + ).not.toThrow(); + }); + + it('accepts a configuration with tsConfigPath and full onlyAudits', () => { + expect(() => + typescriptPluginConfigSchema.parse({ + tsConfigPath, + onlyAudits: [ + 'syntax-errors', + 'semantic-errors', + 'configuration-errors', + ], + } satisfies TypescriptPluginOptions), + ).not.toThrow(); + }); + + it('throws for invalid onlyAudits', () => { + expect(() => + typescriptPluginConfigSchema.parse({ + onlyAudits: 123, + }), + ).toThrow('invalid_type'); + }); + + it('throws for invalid onlyAudits items', () => { + expect(() => + typescriptPluginConfigSchema.parse({ + tsConfigPath, + onlyAudits: [123, true], + }), + ).toThrow('invalid_type'); + }); + + it('throws for unknown audit slug', () => { + expect( + () => + typescriptPluginConfigSchema.parse({ + tsConfigPath, + onlyAudits: ['unknown-audit'], + }), + // Message too large because enums validation + // eslint-disable-next-line vitest/require-to-throw-message + ).toThrow(); + }); +}); diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.ts b/packages/plugin-typescript/src/lib/typescript-plugin.ts new file mode 100644 index 000000000..acb5aef8c --- /dev/null +++ b/packages/plugin-typescript/src/lib/typescript-plugin.ts @@ -0,0 +1,57 @@ +import { createRequire } from 'node:module'; +import type { PluginConfig } from '@code-pushup/models'; +import { DEFAULT_TS_CONFIG, TYPESCRIPT_PLUGIN_SLUG } from './constants.js'; +import { createRunnerFunction } from './runner/runner.js'; +import type { DiagnosticsOptions } from './runner/ts-runner.js'; +import { typescriptPluginConfigSchema } from './schema.js'; +import type { AuditSlug } from './types.js'; +import { getAudits, getGroups, logSkippedAudits } from './utils.js'; + +const packageJson = createRequire(import.meta.url)( + '../../package.json', +) as typeof import('../../package.json'); + +export type FilterOptions = { onlyAudits?: AuditSlug[] | undefined }; +export type TypescriptPluginOptions = Partial & + FilterOptions; + +export async function typescriptPlugin( + options?: TypescriptPluginOptions, +): Promise { + const { tsconfig = DEFAULT_TS_CONFIG, onlyAudits } = parseOptions( + options ?? {}, + ); + + const filteredAudits = getAudits({ onlyAudits }); + const filteredGroups = getGroups({ onlyAudits }); + + logSkippedAudits(filteredAudits); + + return { + slug: TYPESCRIPT_PLUGIN_SLUG, + packageName: packageJson.name, + version: packageJson.version, + title: 'Typescript', + description: 'Official Code PushUp Typescript plugin.', + docsUrl: 'https://www.npmjs.com/package/@code-pushup/typescript-plugin/', + icon: 'typescript', + audits: filteredAudits, + groups: filteredGroups, + runner: createRunnerFunction({ + tsconfig, + expectedAudits: filteredAudits, + }), + }; +} + +function parseOptions( + tsPluginOptions: TypescriptPluginOptions, +): TypescriptPluginOptions { + try { + return typescriptPluginConfigSchema.parse(tsPluginOptions); + } catch (error) { + throw new Error( + `Error parsing TypeScript Plugin options: ${(error as Error).message}`, + ); + } +} diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts new file mode 100644 index 000000000..8169eafab --- /dev/null +++ b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts @@ -0,0 +1,31 @@ +import { expect } from 'vitest'; +import { pluginConfigSchema } from '@code-pushup/models'; +import { AUDITS, GROUPS } from './constants.js'; +import { typescriptPlugin } from './typescript-plugin.js'; + +describe('typescriptPlugin-config-object', () => { + it('should create valid plugin config without options', async () => { + const pluginConfig = await typescriptPlugin(); + + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + + const { audits, groups } = pluginConfig; + expect(audits).toHaveLength(AUDITS.length); + expect(groups).toBeDefined(); + expect(groups!).toHaveLength(GROUPS.length); + }); + + it('should create valid plugin config', async () => { + const pluginConfig = await typescriptPlugin({ + tsConfigPath: 'mocked-away/tsconfig.json', + onlyAudits: ['syntax-errors', 'semantic-errors', 'configuration-errors'], + }); + + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + + const { audits, groups } = pluginConfig; + expect(audits).toHaveLength(3); + expect(groups).toBeDefined(); + expect(groups!).toHaveLength(2); + }); +}); diff --git a/packages/utils/src/lib/string.ts b/packages/utils/src/lib/string.ts new file mode 100644 index 000000000..516c12289 --- /dev/null +++ b/packages/utils/src/lib/string.ts @@ -0,0 +1,58 @@ +import type { CamelCaseToKebabCase } from './types'; + +/** + * Converts a kebab-case string to camelCase. + * @param string - The kebab-case string to convert. + * @returns The camelCase string. + */ +export function kebabCaseToCamelCase(string: string) { + return string + .split('-') + .map((segment, index) => + index === 0 + ? segment + : segment.charAt(0).toUpperCase() + segment.slice(1), + ) + .join(''); +} + +/** + * Converts a camelCase string to kebab-case. + * @param string - The camelCase string to convert. + * @returns The kebab-case string. + */ +export function camelCaseToKebabCase( + string: T, +): CamelCaseToKebabCase { + return string + .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase + .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase + .replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens + .toLowerCase() as CamelCaseToKebabCase; +} + +/** + * Formats a slug to a readable title. + * @param slug - The slug to format. + * @returns The formatted title. + */ +export function kebabCaseToSentence(slug: string = '') { + return slug + .replace(/-/g, ' ') + .replace(/\b\w/g, letter => letter.toUpperCase()); +} + +/** + * Formats a slug to a readable title. + * @param slug - The slug to format. + * @returns The formatted title. + */ +export function camelCaseToSentence(slug: string = '') { + return slug + .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase + .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase + .replace(/[\s_]+/g, ' ') // Replace spaces and underscores with hyphens + .replace(/\b\w/g, letter => letter.toUpperCase()); +} diff --git a/packages/utils/src/lib/string.unit.test.ts b/packages/utils/src/lib/string.unit.test.ts new file mode 100644 index 000000000..d7100c984 --- /dev/null +++ b/packages/utils/src/lib/string.unit.test.ts @@ -0,0 +1,81 @@ +import { + camelCaseToKebabCase, + kebabCaseToCamelCase, + kebabCaseToSentence, +} from './string.js'; + +describe('kebabCaseToCamelCase', () => { + it('should convert simple kebab-case to camelCase', () => { + expect(kebabCaseToCamelCase('hello-world')).toBe('helloWorld'); + }); + + it('should handle multiple hyphens', () => { + expect(kebabCaseToCamelCase('this-is-a-long-string')).toBe( + 'thisIsALongString', + ); + }); + + it('should preserve numbers', () => { + expect(kebabCaseToCamelCase('user-123-test')).toBe('user123Test'); + }); + + it('should handle single word', () => { + expect(kebabCaseToCamelCase('hello')).toBe('hello'); + }); + + it('should handle empty string', () => { + expect(kebabCaseToCamelCase('')).toBe(''); + }); +}); + +describe('camelCaseToKebabCase', () => { + it('should convert simple camelCase to kebab-case', () => { + expect(camelCaseToKebabCase('helloWorld')).toBe('hello-world'); + }); + + it('should handle multiple capital letters', () => { + expect(camelCaseToKebabCase('thisIsALongString')).toBe( + 'this-is-a-long-string', + ); + }); + + it('should handle consecutive capital letters', () => { + expect(camelCaseToKebabCase('myXMLParser')).toBe('my-xml-parser'); + }); + + it('should handle spaces and underscores', () => { + expect(camelCaseToKebabCase('hello_world test')).toBe('hello-world-test'); + }); + + it('should handle single word', () => { + expect(camelCaseToKebabCase('hello')).toBe('hello'); + }); + + it('should handle empty string', () => { + expect(camelCaseToKebabCase('')).toBe(''); + }); +}); + +describe('kebabCaseToSentence', () => { + it('should convert simple slug to title case', () => { + expect(kebabCaseToSentence('hello-world')).toBe('Hello World'); + }); + + it('should handle multiple hyphens', () => { + expect(kebabCaseToSentence('this-is-a-title')).toBe('This Is A Title'); + }); + + it('should handle empty string', () => { + expect(kebabCaseToSentence()).toBe(''); + }); + + it('should handle single word', () => { + expect(kebabCaseToSentence('hello')).toBe('Hello'); + }); + + it('should handle numbers in slug', () => { + expect(kebabCaseToSentence('chapter-1-introduction')).toBe( + 'Chapter 1 Introduction', + ); + }); +}); From 0888cc526dfaf50f800cd04b815b73e14b5b1b5e Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 Feb 2025 16:43:21 +0100 Subject: [PATCH 02/13] feat(plugin-typescript): adjust types --- packages/plugin-typescript/src/lib/schema.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/plugin-typescript/src/lib/schema.ts b/packages/plugin-typescript/src/lib/schema.ts index 54270bcc2..c23c95521 100644 --- a/packages/plugin-typescript/src/lib/schema.ts +++ b/packages/plugin-typescript/src/lib/schema.ts @@ -1,11 +1,7 @@ import { z } from 'zod'; import { AUDITS, DEFAULT_TS_CONFIG } from './constants.js'; -import type { AuditSlug } from './types.js'; -const auditSlugs = AUDITS.map(({ slug }) => slug) as [ - AuditSlug, - ...AuditSlug[], -]; +const auditSlugs = AUDITS.map(({ slug }) => slug) as [string, ...string[]]; export const typescriptPluginConfigSchema = z.object({ tsconfig: z .string({ From fa8fc9d0642fb1c47845be33771d9340e1dfeb01 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 Feb 2025 16:46:22 +0100 Subject: [PATCH 03/13] test(plugin-typescript): refine tests --- .../plugin-typescript/src/lib/schema.unit.test.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/plugin-typescript/src/lib/schema.unit.test.ts b/packages/plugin-typescript/src/lib/schema.unit.test.ts index fd09145f6..1efd1eabd 100644 --- a/packages/plugin-typescript/src/lib/schema.unit.test.ts +++ b/packages/plugin-typescript/src/lib/schema.unit.test.ts @@ -59,14 +59,11 @@ describe('typescriptPluginConfigSchema', () => { }); it('throws for unknown audit slug', () => { - expect( - () => - typescriptPluginConfigSchema.parse({ - tsConfigPath, - onlyAudits: ['unknown-audit'], - }), - // Message too large because enums validation - // eslint-disable-next-line vitest/require-to-throw-message - ).toThrow(); + expect(() => + typescriptPluginConfigSchema.parse({ + tsConfigPath, + onlyAudits: ['unknown-audit'], + }), + ).toThrow(/unknown-audit/); }); }); From dd8e85e52f30e1e8d4a39348e0d29e978eba2778 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 Feb 2025 16:50:18 +0100 Subject: [PATCH 04/13] test(plugin-typescript): refine types --- packages/plugin-typescript/src/lib/typescript-plugin.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.ts b/packages/plugin-typescript/src/lib/typescript-plugin.ts index acb5aef8c..d03f7af5d 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.ts @@ -1,5 +1,6 @@ import { createRequire } from 'node:module'; import type { PluginConfig } from '@code-pushup/models'; +import { stringifyError } from '@code-pushup/utils'; import { DEFAULT_TS_CONFIG, TYPESCRIPT_PLUGIN_SLUG } from './constants.js'; import { createRunnerFunction } from './runner/runner.js'; import type { DiagnosticsOptions } from './runner/ts-runner.js'; @@ -11,7 +12,7 @@ const packageJson = createRequire(import.meta.url)( '../../package.json', ) as typeof import('../../package.json'); -export type FilterOptions = { onlyAudits?: AuditSlug[] | undefined }; +export type FilterOptions = { onlyAudits?: AuditSlug[] }; export type TypescriptPluginOptions = Partial & FilterOptions; @@ -48,10 +49,10 @@ function parseOptions( tsPluginOptions: TypescriptPluginOptions, ): TypescriptPluginOptions { try { - return typescriptPluginConfigSchema.parse(tsPluginOptions); + return typescriptPluginConfigSchema.parse(tsPluginOptions) as FilterOptions; } catch (error) { throw new Error( - `Error parsing TypeScript Plugin options: ${(error as Error).message}`, + `Error parsing TypeScript Plugin options: ${stringifyError(error)}`, ); } } From 1aea4c045981ec5deb7b93e50f67d3e8599adbfc Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 Feb 2025 16:52:12 +0100 Subject: [PATCH 05/13] test(plugin-typescript): remove old code --- packages/utils/src/lib/string.ts | 58 ---------------- packages/utils/src/lib/string.unit.test.ts | 81 ---------------------- 2 files changed, 139 deletions(-) delete mode 100644 packages/utils/src/lib/string.ts delete mode 100644 packages/utils/src/lib/string.unit.test.ts diff --git a/packages/utils/src/lib/string.ts b/packages/utils/src/lib/string.ts deleted file mode 100644 index 516c12289..000000000 --- a/packages/utils/src/lib/string.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { CamelCaseToKebabCase } from './types'; - -/** - * Converts a kebab-case string to camelCase. - * @param string - The kebab-case string to convert. - * @returns The camelCase string. - */ -export function kebabCaseToCamelCase(string: string) { - return string - .split('-') - .map((segment, index) => - index === 0 - ? segment - : segment.charAt(0).toUpperCase() + segment.slice(1), - ) - .join(''); -} - -/** - * Converts a camelCase string to kebab-case. - * @param string - The camelCase string to convert. - * @returns The kebab-case string. - */ -export function camelCaseToKebabCase( - string: T, -): CamelCaseToKebabCase { - return string - .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase - .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase - .replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens - .toLowerCase() as CamelCaseToKebabCase; -} - -/** - * Formats a slug to a readable title. - * @param slug - The slug to format. - * @returns The formatted title. - */ -export function kebabCaseToSentence(slug: string = '') { - return slug - .replace(/-/g, ' ') - .replace(/\b\w/g, letter => letter.toUpperCase()); -} - -/** - * Formats a slug to a readable title. - * @param slug - The slug to format. - * @returns The formatted title. - */ -export function camelCaseToSentence(slug: string = '') { - return slug - .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase - .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase - .replace(/[\s_]+/g, ' ') // Replace spaces and underscores with hyphens - .replace(/\b\w/g, letter => letter.toUpperCase()); -} diff --git a/packages/utils/src/lib/string.unit.test.ts b/packages/utils/src/lib/string.unit.test.ts deleted file mode 100644 index d7100c984..000000000 --- a/packages/utils/src/lib/string.unit.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - camelCaseToKebabCase, - kebabCaseToCamelCase, - kebabCaseToSentence, -} from './string.js'; - -describe('kebabCaseToCamelCase', () => { - it('should convert simple kebab-case to camelCase', () => { - expect(kebabCaseToCamelCase('hello-world')).toBe('helloWorld'); - }); - - it('should handle multiple hyphens', () => { - expect(kebabCaseToCamelCase('this-is-a-long-string')).toBe( - 'thisIsALongString', - ); - }); - - it('should preserve numbers', () => { - expect(kebabCaseToCamelCase('user-123-test')).toBe('user123Test'); - }); - - it('should handle single word', () => { - expect(kebabCaseToCamelCase('hello')).toBe('hello'); - }); - - it('should handle empty string', () => { - expect(kebabCaseToCamelCase('')).toBe(''); - }); -}); - -describe('camelCaseToKebabCase', () => { - it('should convert simple camelCase to kebab-case', () => { - expect(camelCaseToKebabCase('helloWorld')).toBe('hello-world'); - }); - - it('should handle multiple capital letters', () => { - expect(camelCaseToKebabCase('thisIsALongString')).toBe( - 'this-is-a-long-string', - ); - }); - - it('should handle consecutive capital letters', () => { - expect(camelCaseToKebabCase('myXMLParser')).toBe('my-xml-parser'); - }); - - it('should handle spaces and underscores', () => { - expect(camelCaseToKebabCase('hello_world test')).toBe('hello-world-test'); - }); - - it('should handle single word', () => { - expect(camelCaseToKebabCase('hello')).toBe('hello'); - }); - - it('should handle empty string', () => { - expect(camelCaseToKebabCase('')).toBe(''); - }); -}); - -describe('kebabCaseToSentence', () => { - it('should convert simple slug to title case', () => { - expect(kebabCaseToSentence('hello-world')).toBe('Hello World'); - }); - - it('should handle multiple hyphens', () => { - expect(kebabCaseToSentence('this-is-a-title')).toBe('This Is A Title'); - }); - - it('should handle empty string', () => { - expect(kebabCaseToSentence()).toBe(''); - }); - - it('should handle single word', () => { - expect(kebabCaseToSentence('hello')).toBe('Hello'); - }); - - it('should handle numbers in slug', () => { - expect(kebabCaseToSentence('chapter-1-introduction')).toBe( - 'Chapter 1 Introduction', - ); - }); -}); From 100fa0d657b195702d79faf311098c3f343d2dde Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 Feb 2025 16:58:09 +0100 Subject: [PATCH 06/13] fix(plugin-typescript): add missing deps --- packages/plugin-typescript/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/plugin-typescript/package.json b/packages/plugin-typescript/package.json index d7d9831b7..c8c441b15 100644 --- a/packages/plugin-typescript/package.json +++ b/packages/plugin-typescript/package.json @@ -24,7 +24,8 @@ "type": "module", "dependencies": { "@code-pushup/models": "0.59.0", - "@code-pushup/utils": "0.59.0" + "@code-pushup/utils": "0.59.0", + "zod": "^3.23.8" }, "peerDependencies": { "typescript": ">=4.0.0" From e3dd722c2d4f097b040de5e0795856b0c176bd31 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 Feb 2025 17:23:02 +0100 Subject: [PATCH 07/13] test(plugin-typescript): add test case --- .../src/lib/typescript-plugin.unit.test.ts | 53 +++++++++++-------- packages/plugin-typescript/src/lib/utils.ts | 5 ++ 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts index 8169eafab..fd58563f7 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts @@ -1,31 +1,38 @@ -import { expect } from 'vitest'; -import { pluginConfigSchema } from '@code-pushup/models'; -import { AUDITS, GROUPS } from './constants.js'; -import { typescriptPlugin } from './typescript-plugin.js'; +import {expect} from 'vitest'; +import {pluginConfigSchema} from '@code-pushup/models'; +import {AUDITS, GROUPS} from './constants.js'; +import {typescriptPlugin} from './typescript-plugin.js'; +import type {TypescriptPluginOptions} from "./types"; describe('typescriptPlugin-config-object', () => { - it('should create valid plugin config without options', async () => { - const pluginConfig = await typescriptPlugin(); + it('should create valid plugin config without options', async () => { + const pluginConfig = await typescriptPlugin(); - expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); - const { audits, groups } = pluginConfig; - expect(audits).toHaveLength(AUDITS.length); - expect(groups).toBeDefined(); - expect(groups!).toHaveLength(GROUPS.length); - }); - - it('should create valid plugin config', async () => { - const pluginConfig = await typescriptPlugin({ - tsConfigPath: 'mocked-away/tsconfig.json', - onlyAudits: ['syntax-errors', 'semantic-errors', 'configuration-errors'], + const {audits, groups} = pluginConfig; + expect(audits).toHaveLength(AUDITS.length); + expect(groups).toBeDefined(); + expect(groups!).toHaveLength(GROUPS.length); }); - expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + it('should create valid plugin config', async () => { + const pluginConfig = await typescriptPlugin({ + tsconfig: 'mocked-away/tsconfig.json', + onlyAudits: ['syntax-errors', 'semantic-errors', 'configuration-errors'], + }); + + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); - const { audits, groups } = pluginConfig; - expect(audits).toHaveLength(3); - expect(groups).toBeDefined(); - expect(groups!).toHaveLength(2); - }); + const {audits, groups} = pluginConfig; + expect(audits).toHaveLength(3); + expect(groups).toBeDefined(); + expect(groups!).toHaveLength(2); + }); + + it('should throw for invalid valid params', async () => { + await expect(() => typescriptPlugin({ + tsconfig: 42 + } as unknown as TypescriptPluginOptions)).rejects.toThrow(/invalid_type/); + }); }); diff --git a/packages/plugin-typescript/src/lib/utils.ts b/packages/plugin-typescript/src/lib/utils.ts index a678fa5ac..3ce63b95a 100644 --- a/packages/plugin-typescript/src/lib/utils.ts +++ b/packages/plugin-typescript/src/lib/utils.ts @@ -4,6 +4,11 @@ import { kebabCaseToCamelCase } from '@code-pushup/utils'; import { AUDITS, GROUPS, TYPESCRIPT_PLUGIN_SLUG } from './constants.js'; import type { FilterOptions, TypescriptPluginOptions } from './types.js'; +/** + * It filters the audits by the slugs + * + * @param slugs + */ export function filterAuditsBySlug(slugs?: string[]) { return ({ slug }: { slug: string }) => { if (slugs && slugs.length > 0) { From f548eeaf8a47d419783c60df0ea35683426648d5 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 Feb 2025 17:25:22 +0100 Subject: [PATCH 08/13] fix(plugin-typescript): fix lint, format code --- .../src/lib/typescript-plugin.unit.test.ts | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts index fd58563f7..5566bb995 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts @@ -1,38 +1,40 @@ -import {expect} from 'vitest'; -import {pluginConfigSchema} from '@code-pushup/models'; -import {AUDITS, GROUPS} from './constants.js'; -import {typescriptPlugin} from './typescript-plugin.js'; -import type {TypescriptPluginOptions} from "./types"; +import { expect } from 'vitest'; +import { pluginConfigSchema } from '@code-pushup/models'; +import { AUDITS, GROUPS } from './constants.js'; +import type { TypescriptPluginOptions } from './types.js'; +import { typescriptPlugin } from './typescript-plugin.js'; describe('typescriptPlugin-config-object', () => { - it('should create valid plugin config without options', async () => { - const pluginConfig = await typescriptPlugin(); + it('should create valid plugin config without options', async () => { + const pluginConfig = await typescriptPlugin(); - expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); - const {audits, groups} = pluginConfig; - expect(audits).toHaveLength(AUDITS.length); - expect(groups).toBeDefined(); - expect(groups!).toHaveLength(GROUPS.length); - }); + const { audits, groups } = pluginConfig; + expect(audits).toHaveLength(AUDITS.length); + expect(groups).toBeDefined(); + expect(groups!).toHaveLength(GROUPS.length); + }); - it('should create valid plugin config', async () => { - const pluginConfig = await typescriptPlugin({ - tsconfig: 'mocked-away/tsconfig.json', - onlyAudits: ['syntax-errors', 'semantic-errors', 'configuration-errors'], - }); + it('should create valid plugin config', async () => { + const pluginConfig = await typescriptPlugin({ + tsconfig: 'mocked-away/tsconfig.json', + onlyAudits: ['syntax-errors', 'semantic-errors', 'configuration-errors'], + }); - expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); - const {audits, groups} = pluginConfig; - expect(audits).toHaveLength(3); - expect(groups).toBeDefined(); - expect(groups!).toHaveLength(2); - }); + const { audits, groups } = pluginConfig; + expect(audits).toHaveLength(3); + expect(groups).toBeDefined(); + expect(groups!).toHaveLength(2); + }); - it('should throw for invalid valid params', async () => { - await expect(() => typescriptPlugin({ - tsconfig: 42 - } as unknown as TypescriptPluginOptions)).rejects.toThrow(/invalid_type/); - }); + it('should throw for invalid valid params', async () => { + await expect(() => + typescriptPlugin({ + tsconfig: 42, + } as unknown as TypescriptPluginOptions), + ).rejects.toThrow(/invalid_type/); + }); }); From b1c746627a9b5b6103624841a98c73f6d5d0f453 Mon Sep 17 00:00:00 2001 From: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> Date: Sat, 15 Feb 2025 12:22:42 +0100 Subject: [PATCH 09/13] Update packages/plugin-typescript/src/lib/schema.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Matěj Chalk <34691111+matejchalk@users.noreply.github.com> --- packages/plugin-typescript/src/lib/schema.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/plugin-typescript/src/lib/schema.ts b/packages/plugin-typescript/src/lib/schema.ts index c23c95521..db8a3a395 100644 --- a/packages/plugin-typescript/src/lib/schema.ts +++ b/packages/plugin-typescript/src/lib/schema.ts @@ -1,20 +1,27 @@ import { z } from 'zod'; import { AUDITS, DEFAULT_TS_CONFIG } from './constants.js'; +import type { AuditSlug } from './types.js'; -const auditSlugs = AUDITS.map(({ slug }) => slug) as [string, ...string[]]; +const auditSlugs = AUDITS.map(({ slug }) => slug) as [ + AuditSlug, + ...AuditSlug[], +]; export const typescriptPluginConfigSchema = z.object({ tsconfig: z .string({ - description: 'Path to the TsConfig', + description: 'Path to a tsconfig file (default is tsconfig.json)', }) .default(DEFAULT_TS_CONFIG), onlyAudits: z .array(z.enum(auditSlugs), { - description: 'Array with specific TsCodes to measure', + description: 'Filters TypeScript compiler errors by diagnostic codes', }) .optional(), }); -export type TypescriptPluginOptions = z.infer< +export type TypescriptPluginOptions = z.input< + typeof typescriptPluginConfigSchema +>; +export type TypescriptPluginConfig = z.infer< typeof typescriptPluginConfigSchema >; From 73d4b10b86abeef5b5e91574c15c02d3cc7371c0 Mon Sep 17 00:00:00 2001 From: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> Date: Sat, 15 Feb 2025 12:22:54 +0100 Subject: [PATCH 10/13] Update packages/plugin-typescript/src/lib/typescript-plugin.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Matěj Chalk <34691111+matejchalk@users.noreply.github.com> --- packages/plugin-typescript/src/lib/typescript-plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.ts b/packages/plugin-typescript/src/lib/typescript-plugin.ts index d03f7af5d..5510381ca 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.ts @@ -47,9 +47,9 @@ export async function typescriptPlugin( function parseOptions( tsPluginOptions: TypescriptPluginOptions, -): TypescriptPluginOptions { +): TypescriptPluginConfig { try { - return typescriptPluginConfigSchema.parse(tsPluginOptions) as FilterOptions; + return typescriptPluginConfigSchema.parse(tsPluginOptions); } catch (error) { throw new Error( `Error parsing TypeScript Plugin options: ${stringifyError(error)}`, From fb1bbad07e6da08c9b7c7ec01e98e8d7512219ea Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Feb 2025 12:34:36 +0100 Subject: [PATCH 11/13] fix(plugin-typescript): refine types --- .../plugin-typescript/src/lib/schema.unit.test.ts | 12 ++++++------ packages/plugin-typescript/src/lib/types.ts | 5 ----- .../plugin-typescript/src/lib/typescript-plugin.ts | 12 +++++------- .../src/lib/typescript-plugin.unit.test.ts | 2 +- packages/plugin-typescript/src/lib/utils.ts | 13 +++++++++---- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/plugin-typescript/src/lib/schema.unit.test.ts b/packages/plugin-typescript/src/lib/schema.unit.test.ts index 1efd1eabd..36208b7fb 100644 --- a/packages/plugin-typescript/src/lib/schema.unit.test.ts +++ b/packages/plugin-typescript/src/lib/schema.unit.test.ts @@ -5,7 +5,7 @@ import { } from './schema.js'; describe('typescriptPluginConfigSchema', () => { - const tsConfigPath = 'tsconfig.json'; + const tsconfig = 'tsconfig.json'; it('accepts a empty configuration', () => { expect(() => typescriptPluginConfigSchema.parse({})).not.toThrow(); @@ -14,7 +14,7 @@ describe('typescriptPluginConfigSchema', () => { it('accepts a configuration with tsConfigPath set', () => { expect(() => typescriptPluginConfigSchema.parse({ - tsConfigPath, + tsconfig, } satisfies TypescriptPluginOptions), ).not.toThrow(); }); @@ -22,7 +22,7 @@ describe('typescriptPluginConfigSchema', () => { it('accepts a configuration with tsConfigPath and empty onlyAudits', () => { expect(() => typescriptPluginConfigSchema.parse({ - tsConfigPath, + tsconfig, onlyAudits: [], } satisfies TypescriptPluginOptions), ).not.toThrow(); @@ -31,7 +31,7 @@ describe('typescriptPluginConfigSchema', () => { it('accepts a configuration with tsConfigPath and full onlyAudits', () => { expect(() => typescriptPluginConfigSchema.parse({ - tsConfigPath, + tsconfig, onlyAudits: [ 'syntax-errors', 'semantic-errors', @@ -52,7 +52,7 @@ describe('typescriptPluginConfigSchema', () => { it('throws for invalid onlyAudits items', () => { expect(() => typescriptPluginConfigSchema.parse({ - tsConfigPath, + tsconfig, onlyAudits: [123, true], }), ).toThrow('invalid_type'); @@ -61,7 +61,7 @@ describe('typescriptPluginConfigSchema', () => { it('throws for unknown audit slug', () => { expect(() => typescriptPluginConfigSchema.parse({ - tsConfigPath, + tsconfig, onlyAudits: ['unknown-audit'], }), ).toThrow(/unknown-audit/); diff --git a/packages/plugin-typescript/src/lib/types.ts b/packages/plugin-typescript/src/lib/types.ts index 7345bb3f0..7c5a6f3e2 100644 --- a/packages/plugin-typescript/src/lib/types.ts +++ b/packages/plugin-typescript/src/lib/types.ts @@ -1,8 +1,3 @@ -import type { DiagnosticsOptions } from './runner/ts-runner.js'; import type { CodeRangeName } from './runner/types.js'; export type AuditSlug = CodeRangeName; - -export type FilterOptions = { onlyAudits?: AuditSlug[] | undefined }; -export type TypescriptPluginOptions = Partial & - FilterOptions; diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.ts b/packages/plugin-typescript/src/lib/typescript-plugin.ts index 5510381ca..345f7855d 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.ts @@ -3,19 +3,17 @@ import type { PluginConfig } from '@code-pushup/models'; import { stringifyError } from '@code-pushup/utils'; import { DEFAULT_TS_CONFIG, TYPESCRIPT_PLUGIN_SLUG } from './constants.js'; import { createRunnerFunction } from './runner/runner.js'; -import type { DiagnosticsOptions } from './runner/ts-runner.js'; -import { typescriptPluginConfigSchema } from './schema.js'; -import type { AuditSlug } from './types.js'; +import { + type TypescriptPluginConfig, + type TypescriptPluginOptions, + typescriptPluginConfigSchema, +} from './schema.js'; import { getAudits, getGroups, logSkippedAudits } from './utils.js'; const packageJson = createRequire(import.meta.url)( '../../package.json', ) as typeof import('../../package.json'); -export type FilterOptions = { onlyAudits?: AuditSlug[] }; -export type TypescriptPluginOptions = Partial & - FilterOptions; - export async function typescriptPlugin( options?: TypescriptPluginOptions, ): Promise { diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts index 5566bb995..838298b7d 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts @@ -1,7 +1,7 @@ import { expect } from 'vitest'; import { pluginConfigSchema } from '@code-pushup/models'; import { AUDITS, GROUPS } from './constants.js'; -import type { TypescriptPluginOptions } from './types.js'; +import type { TypescriptPluginOptions } from './schema.js'; import { typescriptPlugin } from './typescript-plugin.js'; describe('typescriptPlugin-config-object', () => { diff --git a/packages/plugin-typescript/src/lib/utils.ts b/packages/plugin-typescript/src/lib/utils.ts index 3ce63b95a..059d057f5 100644 --- a/packages/plugin-typescript/src/lib/utils.ts +++ b/packages/plugin-typescript/src/lib/utils.ts @@ -1,8 +1,11 @@ import type { CompilerOptions } from 'typescript'; import type { Audit, CategoryConfig, CategoryRef } from '@code-pushup/models'; -import { kebabCaseToCamelCase } from '@code-pushup/utils'; +import { kebabCaseToCamelCase, ui } from '@code-pushup/utils'; import { AUDITS, GROUPS, TYPESCRIPT_PLUGIN_SLUG } from './constants.js'; -import type { FilterOptions, TypescriptPluginOptions } from './types.js'; +import type { + TypescriptPluginConfig, + TypescriptPluginOptions, +} from './schema.js'; /** * It filters the audits by the slugs @@ -63,7 +66,9 @@ export function getGroups(options?: TypescriptPluginOptions) { })).filter(group => group.refs.length > 0); } -export function getAudits(options?: FilterOptions) { +export function getAudits( + options?: Pick, +) { return AUDITS.filter(filterAuditsBySlug(options?.onlyAudits)); } @@ -141,6 +146,6 @@ export function logSkippedAudits(audits: Audit[]) { audit => !audits.some(filtered => filtered.slug === audit.slug), ).map(audit => kebabCaseToCamelCase(audit.slug)); if (skippedAudits.length > 0) { - console.warn(`Skipped audits: [${skippedAudits.join(', ')}]`); + ui().logger.info(`Skipped audits: [${skippedAudits.join(', ')}]`); } } From 169df8048a7bfc86c1464b0bfa8bc7c993997dc4 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Feb 2025 13:01:16 +0100 Subject: [PATCH 12/13] fix(plugin-typescript): refine testing types; use ui logger --- .../src/lib/schema.unit.test.ts | 6 ++-- .../src/lib/utils.unit.test.ts | 30 ++++++------------- .../plugin-typescript/vite.config.unit.ts | 1 + 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/packages/plugin-typescript/src/lib/schema.unit.test.ts b/packages/plugin-typescript/src/lib/schema.unit.test.ts index 36208b7fb..b23fe6a89 100644 --- a/packages/plugin-typescript/src/lib/schema.unit.test.ts +++ b/packages/plugin-typescript/src/lib/schema.unit.test.ts @@ -11,7 +11,7 @@ describe('typescriptPluginConfigSchema', () => { expect(() => typescriptPluginConfigSchema.parse({})).not.toThrow(); }); - it('accepts a configuration with tsConfigPath set', () => { + it('accepts a configuration with tsconfig set', () => { expect(() => typescriptPluginConfigSchema.parse({ tsconfig, @@ -19,7 +19,7 @@ describe('typescriptPluginConfigSchema', () => { ).not.toThrow(); }); - it('accepts a configuration with tsConfigPath and empty onlyAudits', () => { + it('accepts a configuration with tsconfig and empty onlyAudits', () => { expect(() => typescriptPluginConfigSchema.parse({ tsconfig, @@ -28,7 +28,7 @@ describe('typescriptPluginConfigSchema', () => { ).not.toThrow(); }); - it('accepts a configuration with tsConfigPath and full onlyAudits', () => { + it('accepts a configuration with tsconfig and full onlyAudits', () => { expect(() => typescriptPluginConfigSchema.parse({ tsconfig, diff --git a/packages/plugin-typescript/src/lib/utils.unit.test.ts b/packages/plugin-typescript/src/lib/utils.unit.test.ts index 21f4b96eb..ac538be85 100644 --- a/packages/plugin-typescript/src/lib/utils.unit.test.ts +++ b/packages/plugin-typescript/src/lib/utils.unit.test.ts @@ -1,6 +1,7 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { type Audit, categoryRefSchema } from '@code-pushup/models'; -import { AUDITS } from './constants.js'; +import {describe, expect, it} from 'vitest'; +import {type Audit, categoryRefSchema} from '@code-pushup/models'; +import {ui} from '@code-pushup/utils'; +import {AUDITS} from './constants.js'; import { filterAuditsByCompilerOptions, filterAuditsBySlug, @@ -99,14 +100,14 @@ describe('getCategoryRefsFromGroups', () => { it('should return all groups as categoryRefs if compiler options are given', async () => { const categoryRefs = await getCategoryRefsFromGroups({ - tsConfigPath: 'tsconfig.json', + tsconfig: 'tsconfig.json', }); expect(categoryRefs).toHaveLength(3); }); it('should return a subset of all groups as categoryRefs if compiler options contain onlyAudits filter', async () => { const categoryRefs = await getCategoryRefsFromGroups({ - tsConfigPath: 'tsconfig.json', + tsconfig: 'tsconfig.json', onlyAudits: ['semantic-errors'], }); expect(categoryRefs).toHaveLength(1); @@ -114,16 +115,6 @@ describe('getCategoryRefsFromGroups', () => { }); describe('logSkippedAudits', () => { - beforeEach(() => { - vi.mock('console', () => ({ - warn: vi.fn(), - })); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - it('should not warn when all audits are included', () => { logSkippedAudits(AUDITS); @@ -133,8 +124,8 @@ describe('logSkippedAudits', () => { it('should warn about skipped audits', () => { logSkippedAudits(AUDITS.slice(0, -1)); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith( + expect(ui()).toHaveLogged( + 'info', expect.stringContaining(`Skipped audits: [`), ); }); @@ -142,9 +133,6 @@ describe('logSkippedAudits', () => { it('should camel case the slugs in the audit message', () => { logSkippedAudits(AUDITS.slice(0, -1)); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining(`unknownCodes`), - ); + expect(ui()).toHaveLogged('info', expect.stringContaining(`unknownCodes`)); }); }); diff --git a/packages/plugin-typescript/vite.config.unit.ts b/packages/plugin-typescript/vite.config.unit.ts index e7a8783f0..f4b1337f9 100644 --- a/packages/plugin-typescript/vite.config.unit.ts +++ b/packages/plugin-typescript/vite.config.unit.ts @@ -23,6 +23,7 @@ export default defineConfig({ globalSetup: ['../../global-setup.ts'], setupFiles: [ '../../testing/test-setup/src/lib/cliui.mock.ts', + '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts', '../../testing/test-setup/src/lib/fs.mock.ts', '../../testing/test-setup/src/lib/console.mock.ts', '../../testing/test-setup/src/lib/reset.mocks.ts', From 1d88b675bc41b4d225d7874ded9890b11fb79af5 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Feb 2025 13:06:22 +0100 Subject: [PATCH 13/13] fix(plugin-typescript): format --- packages/plugin-typescript/src/lib/utils.unit.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugin-typescript/src/lib/utils.unit.test.ts b/packages/plugin-typescript/src/lib/utils.unit.test.ts index ac538be85..274ca794c 100644 --- a/packages/plugin-typescript/src/lib/utils.unit.test.ts +++ b/packages/plugin-typescript/src/lib/utils.unit.test.ts @@ -1,7 +1,7 @@ -import {describe, expect, it} from 'vitest'; -import {type Audit, categoryRefSchema} from '@code-pushup/models'; -import {ui} from '@code-pushup/utils'; -import {AUDITS} from './constants.js'; +import { describe, expect, it } from 'vitest'; +import { type Audit, categoryRefSchema } from '@code-pushup/models'; +import { ui } from '@code-pushup/utils'; +import { AUDITS } from './constants.js'; import { filterAuditsByCompilerOptions, filterAuditsBySlug,