diff --git a/packages/plugin-typescript/eslint.config.js b/packages/plugin-typescript/eslint.config.js index 82fec10d3..608b7b26a 100644 --- a/packages/plugin-typescript/eslint.config.js +++ b/packages/plugin-typescript/eslint.config.js @@ -4,7 +4,7 @@ import baseConfig from '../../eslint.config.js'; export default tseslint.config( ...baseConfig, { - files: ['**/*.ts', '!**/default-ts-configs'], + files: ['**/*.ts', '!**/default-ts-configs', '!**/mocks'], languageOptions: { parserOptions: { projectService: true, diff --git a/packages/plugin-typescript/src/lib/constants.ts b/packages/plugin-typescript/src/lib/constants.ts index 6ade18019..dd9e61e54 100644 --- a/packages/plugin-typescript/src/lib/constants.ts +++ b/packages/plugin-typescript/src/lib/constants.ts @@ -1,10 +1,7 @@ import type { Audit, Group } from '@code-pushup/models'; import { camelCaseToKebabCase, kebabCaseToSentence } from '@code-pushup/utils'; -import { - GROUPS_DESCRIPTIONS, - TS_ERROR_CODES, -} from './runner/ts-error-codes.js'; -import type { CompilerOptionName } from './types.js'; +import { TS_ERROR_CODES } from './runner/ts-error-codes.js'; +import type { CompilerOptionName } from './runner/types.js'; export const TYPESCRIPT_PLUGIN_SLUG = 'typescript'; export const DEFAULT_TS_CONFIG = 'tsconfig.json'; @@ -24,13 +21,31 @@ export const AUDITS = Object.values(TS_ERROR_CODES) ]; }, []); -const weights = { +const GROUP_WEIGHTS: Partial> = { // eslint-disable-next-line @typescript-eslint/no-magic-numbers - strictChecks: 3, + strict: 3, typeCheckingBehavior: 2, controlFlowOptions: 2, interopConstraints: 2, }; + +const GROUPS_DESCRIPTIONS: Record = { + languageAndEnvironment: + 'Configuration options for TypeScript language features and runtime environment, including decorators, JSX support, target ECMAScript version, and class field behaviors', + interopConstraints: + 'Settings that control how TypeScript interoperates with other JavaScript code, including module imports/exports and case sensitivity rules', + moduleResolution: + 'Settings that control how TypeScript finds and resolves module imports, including Node.js resolution, package.json exports/imports, and module syntax handling', + typeCheckingBehavior: + 'Configuration for TypeScript type checking strictness and error reporting, including property access rules and method override checking', + controlFlowOptions: + 'Settings that affect code flow analysis, including handling of unreachable code, unused labels, switch statements, and async/generator functions', + strict: + 'Strict type checking options that enable additional compile-time verifications, including null checks, implicit any/this, and function type checking', + buildEmitOptions: + 'Configuration options that control TypeScript output generation, including whether to emit files, how to handle comments and declarations, and settings for output optimization and compatibility helpers', +}; + export const GROUPS: Group[] = Object.entries(TS_ERROR_CODES).map( ([groupSlug, auditMap]) => ({ slug: camelCaseToKebabCase(groupSlug), @@ -39,7 +54,7 @@ export const GROUPS: Group[] = Object.entries(TS_ERROR_CODES).map( GROUPS_DESCRIPTIONS[groupSlug as keyof typeof GROUPS_DESCRIPTIONS], refs: Object.keys(auditMap).map(audit => ({ slug: camelCaseToKebabCase(audit), - weight: weights[audit as keyof typeof weights] ?? 1, + weight: GROUP_WEIGHTS[audit as keyof typeof GROUP_WEIGHTS] ?? 1, })), }), ); diff --git a/packages/plugin-typescript/src/lib/runner/constants.ts b/packages/plugin-typescript/src/lib/runner/constants.ts new file mode 100644 index 000000000..33fdecb87 --- /dev/null +++ b/packages/plugin-typescript/src/lib/runner/constants.ts @@ -0,0 +1,13 @@ +import { camelCaseToKebabCase } from '@code-pushup/utils'; +import { TS_ERROR_CODES } from './ts-error-codes.js'; +import type { CompilerOptionName } from './types.js'; + +/** Build Reverse Lookup Map. It will a map with key as the error code and value as the audit slug. */ +export const AUDIT_LOOKUP = Object.values(TS_ERROR_CODES) + .flatMap(v => Object.entries(v)) + .reduce>((lookup, [name, codes]) => { + codes.forEach((code: number) => + lookup.set(code, camelCaseToKebabCase(name) as CompilerOptionName), + ); + return lookup; + }, new Map()); diff --git a/packages/plugin-typescript/src/lib/runner/runner.ts b/packages/plugin-typescript/src/lib/runner/runner.ts index 35f62a7fb..8a4a7f141 100644 --- a/packages/plugin-typescript/src/lib/runner/runner.ts +++ b/packages/plugin-typescript/src/lib/runner/runner.ts @@ -6,23 +6,19 @@ import type { Issue, RunnerFunction, } from '@code-pushup/models'; -import type { CompilerOptionName, TypescriptPluginOptions } from '../types.js'; -import { getDiagnostics } from './typescript-runner.js'; -import { - AUDIT_LOOKUP, - getIssueFromDiagnostic, - tSCodeToAuditSlug, - validateDiagnostics, -} from './utils.js'; +import type { TypescriptPluginOptions } from '../types.js'; +import { AUDIT_LOOKUP } from './constants.js'; +import { getTypeScriptDiagnostics } from './ts-runner.js'; +import type { CompilerOptionName } from './types.js'; +import { getIssueFromDiagnostic, tSCodeToAuditSlug } from './utils.js'; export type RunnerOptions = TypescriptPluginOptions & { - filteredAudits: Audit[]; + expectedAudits: Audit[]; }; export function createRunnerFunction(options: RunnerOptions): RunnerFunction { return async (): Promise => { - const diagnostics = await getDiagnostics(options.tsConfigPath); - validateDiagnostics(diagnostics); + const diagnostics = await getTypeScriptDiagnostics(options.tsConfigPath); const result: Record< CompilerOptionName, Pick @@ -50,8 +46,8 @@ export function createRunnerFunction(options: RunnerOptions): RunnerFunction { >, ); - return options.filteredAudits.map(audit => { - const { details } = result[audit.slug as CompilerOptionName] ?? {}; + return options.expectedAudits.map(audit => { + const { details } = result[audit.slug as CompilerOptionName]; const issues = details?.issues ?? []; return { ...audit, diff --git a/packages/plugin-typescript/src/lib/runner/ts-error-codes.ts b/packages/plugin-typescript/src/lib/runner/ts-error-codes.ts index ee258c17c..24e8ce4f2 100644 --- a/packages/plugin-typescript/src/lib/runner/ts-error-codes.ts +++ b/packages/plugin-typescript/src/lib/runner/ts-error-codes.ts @@ -1,26 +1,5 @@ /* eslint-disable @typescript-eslint/no-magic-numbers, unicorn/numeric-separators-style */ -export const GROUPS_DESCRIPTIONS = { - languageAndEnvironment: - 'Configuration options for TypeScript language features and runtime environment, including decorators, JSX support, target ECMAScript version, and class field behaviors', - interopConstraints: - 'Settings that control how TypeScript interoperates with other JavaScript code, including module imports/exports and case sensitivity rules', - watchOptions: - 'Configuration for TypeScript watch mode behavior, including file watching strategies and dependency tracking', - projectReferences: - 'Options for managing TypeScript project references, composite projects, and build optimization settings', - moduleResolution: - 'Settings that control how TypeScript finds and resolves module imports, including Node.js resolution, package.json exports/imports, and module syntax handling', - typeCheckingBehavior: - 'Configuration for TypeScript type checking strictness and error reporting, including property access rules and method override checking', - controlFlowOptions: - 'Settings that affect code flow analysis, including handling of unreachable code, unused labels, switch statements, and async/generator functions', - strictChecks: - 'Strict type checking options that enable additional compile-time verifications, including null checks, implicit any/this, and function type checking', - buildEmitOptions: - 'Configuration options that control TypeScript output generation, including whether to emit files, how to handle comments and declarations, and settings for output optimization and compatibility helpers', -}; - /** * Strict grouping: https://github.com/microsoft/TypeScript/blob/56a08250f3516b3f5bc120d6c7ab4450a9a69352/src/compiler/utilities.ts Line 9113 * noImplicitThis: { diff --git a/packages/plugin-typescript/src/lib/runner/typescript-runner.integration.test.ts b/packages/plugin-typescript/src/lib/runner/ts-runner.integration.test.ts similarity index 65% rename from packages/plugin-typescript/src/lib/runner/typescript-runner.integration.test.ts rename to packages/plugin-typescript/src/lib/runner/ts-runner.integration.test.ts index 41b97eaf5..c8076a795 100644 --- a/packages/plugin-typescript/src/lib/runner/typescript-runner.integration.test.ts +++ b/packages/plugin-typescript/src/lib/runner/ts-runner.integration.test.ts @@ -1,13 +1,12 @@ import { describe, expect } from 'vitest'; -import { getTsConfigurationFromPath } from './typescript-runner.js'; +import { getTypeScriptDiagnostics } from './ts-runner.js'; -describe('getTsConfigurationFromPath', () => { +describe('getTypeScriptDiagnostics', () => { it('should accept valid options', async () => { await expect( - getTsConfigurationFromPath({ - tsConfigPath: - 'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.json', - }), + getTypeScriptDiagnostics( + 'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.json', + ), ).resolves.toStrictEqual({ compilerOptions: { configFilePath: undefined, diff --git a/packages/plugin-typescript/src/lib/runner/ts-runner.ts b/packages/plugin-typescript/src/lib/runner/ts-runner.ts new file mode 100644 index 000000000..c8d32a39c --- /dev/null +++ b/packages/plugin-typescript/src/lib/runner/ts-runner.ts @@ -0,0 +1,41 @@ +import { + type CompilerOptions, + type Diagnostic, + createProgram, + getPreEmitDiagnostics, +} from 'typescript'; +import { AUDIT_LOOKUP } from './constants.js'; +import { loadTargetConfig } from './utils.js'; + +export type DiagnosticsOptions = { + fileNames: string[]; + compilerOptions: CompilerOptions; +}; + +export async function getTypeScriptDiagnostics( + tsConfigPath: string, +): Promise { + try { + const { fileNames, options } = await loadTargetConfig(tsConfigPath); + const program = createProgram(fileNames, options); + + const diagnostics = getPreEmitDiagnostics(program); + validateDiagnostics(diagnostics); + + return diagnostics; + } catch (error) { + throw new Error( + `Can't create TS program in getDiagnostics. \n ${(error as Error).message}`, + ); + } +} + +export function validateDiagnostics(diagnostics: readonly Diagnostic[]) { + diagnostics + .filter(({ code }) => !AUDIT_LOOKUP.has(code)) + .forEach(({ code, messageText }) => { + console.warn( + `Diagnostic Warning: The code ${code} is not supported. ${messageText}`, + ); + }); +} diff --git a/packages/plugin-typescript/src/lib/runner/types.ts b/packages/plugin-typescript/src/lib/runner/types.ts new file mode 100644 index 000000000..ed991e340 --- /dev/null +++ b/packages/plugin-typescript/src/lib/runner/types.ts @@ -0,0 +1,9 @@ +import { TS_ERROR_CODES } from './ts-error-codes.js'; + +export type ErrorCodes = typeof TS_ERROR_CODES; + +export type CompilerOptionName = { + [K in keyof ErrorCodes]: keyof ErrorCodes[K]; +}[keyof ErrorCodes]; + +export type SemVerString = `${number}.${number}.${number}`; diff --git a/packages/plugin-typescript/src/lib/runner/typescript-runner.ts b/packages/plugin-typescript/src/lib/runner/typescript-runner.ts deleted file mode 100644 index 922a6ec6e..000000000 --- a/packages/plugin-typescript/src/lib/runner/typescript-runner.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { access, readFile } from 'node:fs/promises'; -// eslint-disable-next-line unicorn/import-style -import { dirname, resolve } from 'node:path'; -import { - type CompilerOptions, - type Diagnostic, - createProgram, - getPreEmitDiagnostics, - parseConfigFileTextToJson, - parseJsonConfigFileContent, - sys, -} from 'typescript'; -import type { TypescriptPluginOptions } from '../types.js'; -import { loadTargetConfig } from '../utils.js'; - -export type DiagnosticsOptions = { - fileNames: string[]; - compilerOptions: CompilerOptions; -}; - -export async function getDiagnostics( - tsConfigPath: string, -): Promise { - try { - const { fileNames, options } = await loadTargetConfig(tsConfigPath); - const program = createProgram(fileNames, options); - return getPreEmitDiagnostics(program); - } catch (error) { - throw new Error( - `Can't create TS program in getDiagnostics. \n ${(error as Error).message}`, - ); - } -} - -export async function getTsConfigurationFromPath( - options: Pick, -): Promise { - const { tsConfigPath } = options; - const configPath = resolve(process.cwd(), tsConfigPath); - const basePath = dirname(configPath); - - try { - await access(configPath); - } catch { - throw new Error(`tsconfig not found at: ${tsConfigPath}`); - } - - const configFile = (await readFile(configPath)).toString(); - - const { config } = parseConfigFileTextToJson(configPath, configFile); - const parsed = parseJsonConfigFileContent(config, sys, basePath); - - const { options: compilerOptions, fileNames } = parsed; - if (fileNames.length === 0) { - throw new Error( - 'No files matched by the TypeScript configuration. Check your "include", "exclude" or "files" settings.', - ); - } - - return { - compilerOptions, - fileNames, - }; -} diff --git a/packages/plugin-typescript/src/lib/runner/utils.ts b/packages/plugin-typescript/src/lib/runner/utils.ts index 5d4040f85..781b196f1 100644 --- a/packages/plugin-typescript/src/lib/runner/utils.ts +++ b/packages/plugin-typescript/src/lib/runner/utils.ts @@ -1,22 +1,24 @@ +import { access } from 'node:fs/promises'; +// eslint-disable-next-line unicorn/import-style +import { dirname } from 'node:path'; import { + type CompilerOptions, type Diagnostic, DiagnosticCategory, + type ParsedCommandLine, flattenDiagnosticMessageText, + parseConfigFileTextToJson, + parseJsonConfigFileContent, + sys, } from 'typescript'; import type { Issue } from '@code-pushup/models'; -import { camelCaseToKebabCase, truncateIssueMessage } from '@code-pushup/utils'; -import type { CompilerOptionName } from '../types.js'; -import { TS_ERROR_CODES } from './ts-error-codes.js'; - -/** Build Reverse Lookup Map. It will a map with key as the error code and value as the audit slug. */ -export const AUDIT_LOOKUP = Object.values(TS_ERROR_CODES) - .flatMap(v => Object.entries(v)) - .reduce>((lookup, [name, codes]) => { - codes.forEach((code: number) => - lookup.set(code, camelCaseToKebabCase(name) as CompilerOptionName), - ); - return lookup; - }, new Map()); +import { + executeProcess, + readTextFile, + truncateIssueMessage, +} from '@code-pushup/utils'; +import { AUDIT_LOOKUP } from './constants.js'; +import type { CompilerOptionName, SemVerString } from './types.js'; /** * Transform the TypeScript error code to the audit slug. @@ -32,18 +34,6 @@ export function tSCodeToAuditSlug(code: number): CompilerOptionName { return knownCode; } -//OK DOOONE, now it's more beautiful, goodbye! let me know when u finish if u want -// I was getting so frustrated of with webstorm sry xD -export function validateDiagnostics(diagnostics: readonly Diagnostic[]) { - diagnostics - .filter(({ code }) => !AUDIT_LOOKUP.has(code)) - .forEach(({ code, messageText }) => { - console.warn( - `Diagnostic Warning: The code ${code} is not supported. ${messageText}`, - ); - }); -} - /** * Get the severity of the issue based on the TypeScript diagnostic category. * - ts.DiagnosticCategory.Warning (1) @@ -98,3 +88,58 @@ export function getIssueFromDiagnostic(diag: Diagnostic) { }, } satisfies Issue; } + +const _TS_CONFIG_MAP = new Map(); +export async function loadTargetConfig(tsConfigPath: string) { + if (_TS_CONFIG_MAP.get(tsConfigPath) === undefined) { + const { config } = parseConfigFileTextToJson( + tsConfigPath, + await readTextFile(tsConfigPath), + ); + + const parsedConfig = parseJsonConfigFileContent( + config, + sys, + dirname(tsConfigPath), + ); + + if (parsedConfig.fileNames.length === 0) { + throw new Error( + 'No files matched by the TypeScript configuration. Check your "include", "exclude" or "files" settings.', + ); + } + + _TS_CONFIG_MAP.set(tsConfigPath, parsedConfig); + } + return _TS_CONFIG_MAP.get(tsConfigPath) as ParsedCommandLine; +} + +export async function getCurrentTsVersion(): Promise { + const { stdout } = await executeProcess({ + command: 'npx', + args: ['-y', 'tsc', '--version'], + }); + return stdout.split(' ').slice(-1).join('').trim() as SemVerString; +} + +export async function loadTsConfigDefaultsByVersion(version: SemVerString) { + const __dirname = new URL('.', import.meta.url).pathname; + const configPath = `${__dirname}default-ts-configs/${version}.ts`; + + try { + await access(configPath); + } catch { + throw new Error( + `Could not find default TS config for version ${version}. R The plugin maintainer has to support this version.`, + ); + } + + try { + const module = await import(configPath); + return module.default as { compilerOptions: CompilerOptions }; + } catch (error) { + throw new Error( + `Could load default TS config for version ${version}. /n ${(error as Error).message}`, + ); + } +} diff --git a/packages/plugin-typescript/src/lib/types.ts b/packages/plugin-typescript/src/lib/types.ts index c680fe37b..c802f9d9a 100644 --- a/packages/plugin-typescript/src/lib/types.ts +++ b/packages/plugin-typescript/src/lib/types.ts @@ -1,24 +1,10 @@ import { z } from 'zod'; -import { TS_ERROR_CODES } from './runner/ts-error-codes.js'; +import type { CamelCaseToKebabCase } from '@code-pushup/utils'; +import type { CompilerOptionName } from './runner/types.js'; import { typescriptPluginConfigSchema } from './schema.js'; -type CamelCaseToKebabCase = - S extends `${infer First}${infer Rest}` - ? Rest extends Uncapitalize - ? `${Lowercase}${CamelCaseToKebabCase}` - : `${Lowercase}-${CamelCaseToKebabCase}` - : S; - -export type SemVerString = `${number}.${number}.${number}`; - -type ErrorCodes = typeof TS_ERROR_CODES; - -export type CompilerOptionName = { - [K in keyof ErrorCodes]: keyof ErrorCodes[K]; -}[keyof ErrorCodes]; - export type AuditSlug = CamelCaseToKebabCase; export type TypescriptPluginOptions = z.infer< typeof typescriptPluginConfigSchema -> & { onlyAudits?: (string | AuditSlug)[] | undefined }; +> & { onlyAudits?: AuditSlug[] | undefined }; diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.ts b/packages/plugin-typescript/src/lib/typescript-plugin.ts index 744362513..ba38cbc8f 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.ts @@ -1,53 +1,39 @@ import type { PluginConfig } from '@code-pushup/models'; -import { kebabCaseToCamelCase } from '@code-pushup/utils'; import { name as packageName, version } from '../../package.json'; -import { - AUDITS, - DEFAULT_TS_CONFIG, - TYPESCRIPT_PLUGIN_SLUG, -} from './constants.js'; +import { DEFAULT_TS_CONFIG, TYPESCRIPT_PLUGIN_SLUG } from './constants.js'; import { createRunnerFunction } from './runner/runner.js'; import type { TypescriptPluginOptions } from './types.js'; import { - filterAuditsByTsOptions, - getCompilerOptionsToDetermineListedAudits, + getAudits, getGroups, + normalizeCompilerOptions, + validateAudits, } from './utils.js'; export async function typescriptPlugin( options?: TypescriptPluginOptions, ): Promise { const { tsConfigPath } = options ?? { tsConfigPath: DEFAULT_TS_CONFIG }; - const definitive = await getCompilerOptionsToDetermineListedAudits(options); - - const filteredAudits = AUDITS.filter( - filterAuditsByTsOptions(definitive, options?.onlyAudits), - ); - const filteredGroups = getGroups(definitive, options?.onlyAudits); - const skippedAudits = AUDITS.filter( - audit => !filteredAudits.some(filtered => filtered.slug === audit.slug), - ).map(audit => kebabCaseToCamelCase(audit.slug)); + const compilerOptions = await normalizeCompilerOptions(options); + const filteredAudits = getAudits(compilerOptions, options); + const filteredGroups = getGroups(compilerOptions, options); - if (skippedAudits.length > 0) { - console.warn( - `Some audits were skipped because the configuration of the compiler options [${skippedAudits.join(', ')}]`, - ); - } + validateAudits(filteredAudits); return { slug: TYPESCRIPT_PLUGIN_SLUG, packageName, version, title: 'Typescript', - description: 'Official Code PushUp typescript plugin.', + description: 'Official Code PushUp Typescript plugin.', docsUrl: 'https://www.npmjs.com/package/@code-pushup/typescript-plugin/', icon: 'typescript', audits: filteredAudits, groups: filteredGroups, runner: createRunnerFunction({ tsConfigPath, - filteredAudits, + expectedAudits: filteredAudits, }), }; } diff --git a/packages/plugin-typescript/src/lib/utils.integration.test.ts b/packages/plugin-typescript/src/lib/utils.integration.test.ts index 551e92774..db0bd4011 100644 --- a/packages/plugin-typescript/src/lib/utils.integration.test.ts +++ b/packages/plugin-typescript/src/lib/utils.integration.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import config544 from './default-ts-configs/5.4.4.js'; import type { TypescriptPluginOptions } from './types.js'; -import { getCompilerOptionsToDetermineListedAudits } from './utils.js'; +import { normalizeCompilerOptions } from './utils.js'; import * as utilsModule from './utils.js'; describe('getCompilerOptions', () => { @@ -14,7 +14,7 @@ describe('getCompilerOptions', () => { 'packages/plugin-typescript/mocks/fixtures/compiler-defaults/tsconfig.json', }; - const definitive = await getCompilerOptionsToDetermineListedAudits(options); + const definitive = await normalizeCompilerOptions(options); const { importsNotUsedAsValues, preserveValueImports, ...parsedOptions } = config544.compilerOptions; expect(definitive).toStrictEqual( @@ -37,7 +37,7 @@ describe('getCompilerOptions', () => { 'packages/plugin-typescript/mocks/fixtures/compiler-defaults/tsconfig.json', }; - const definitive = await getCompilerOptionsToDetermineListedAudits(options); + const definitive = await normalizeCompilerOptions(options); expect(definitive).toStrictEqual( expect.objectContaining({ strict: true, diff --git a/packages/plugin-typescript/src/lib/utils.ts b/packages/plugin-typescript/src/lib/utils.ts index ca6262db4..0c91a78f4 100644 --- a/packages/plugin-typescript/src/lib/utils.ts +++ b/packages/plugin-typescript/src/lib/utils.ts @@ -1,31 +1,19 @@ -import { access } from 'node:fs/promises'; -// eslint-disable-next-line unicorn/import-style -import { dirname } from 'node:path'; -import { - type CompilerOptions, - type ParsedCommandLine, - parseConfigFileTextToJson, - parseJsonConfigFileContent, - sys, -} from 'typescript'; -import type { CategoryRef, Group } from '@code-pushup/models'; -import { - camelCaseToKebabCase, - executeProcess, - kebabCaseToCamelCase, - readTextFile, -} from '@code-pushup/utils'; +import type { CompilerOptions } from 'typescript'; +import type { Audit, CategoryRef } from '@code-pushup/models'; +import { kebabCaseToCamelCase } from '@code-pushup/utils'; import { + AUDITS, DEFAULT_TS_CONFIG, GROUPS, TYPESCRIPT_PLUGIN_SLUG, } from './constants.js'; import { TS_ERROR_CODES } from './runner/ts-error-codes.js'; -import type { - AuditSlug, - SemVerString, - TypescriptPluginOptions, -} from './types.js'; +import { + getCurrentTsVersion, + loadTargetConfig, + loadTsConfigDefaultsByVersion, +} from './runner/utils.js'; +import type { TypescriptPluginOptions } from './types.js'; export function filterAuditsBySlug(slugs?: string[]) { return ({ slug }: { slug: string }) => { @@ -43,7 +31,7 @@ export function filterAuditsBySlug(slugs?: string[]) { * @param slug Slug to be transformed * @returns The slug as compilerOption key */ -export function auditSlugToCompilerOption(slug: string): string { +function auditSlugToCompilerOption(slug: string): string { // eslint-disable-next-line sonarjs/no-small-switch switch (slug) { case 'emit-bom': @@ -60,7 +48,7 @@ export function auditSlugToCompilerOption(slug: string): string { * @param onlyAudits OnlyAudits * @returns Filtered Audits */ -export function filterAuditsByTsOptions( +export function filterAuditsByCompilerOptions( compilerOptions: CompilerOptions, onlyAudits?: string[], ) { @@ -72,22 +60,30 @@ export function filterAuditsByTsOptions( }; } -export function filterGroupsByAuditSlug(slugs?: string[]) { - return ({ refs }: Group) => refs.some(filterAuditsBySlug(slugs)); -} - export function getGroups( compilerOptions: CompilerOptions, - onlyAudits?: string[], + options?: TypescriptPluginOptions, ) { return GROUPS.map(group => ({ ...group, refs: group.refs.filter( - filterAuditsByTsOptions(compilerOptions, onlyAudits), + filterAuditsByCompilerOptions( + compilerOptions, + (options ?? {})?.onlyAudits, + ), ), })).filter(group => group.refs.length > 0); } +export function getAudits( + definitive: CompilerOptions, + options?: TypescriptPluginOptions, +) { + return AUDITS.filter( + filterAuditsByCompilerOptions(definitive, options?.onlyAudits), + ); +} + /** * Retrieve the category references from the groups (already processed from the audits). * Used in the code-pushup preset @@ -97,11 +93,11 @@ export function getGroups( export async function getCategoryRefsFromGroups( opt?: TypescriptPluginOptions, ): Promise { - const definitive = await getCompilerOptionsToDetermineListedAudits(opt); + const definitive = await normalizeCompilerOptions(opt); return GROUPS.map(group => ({ ...group, refs: group.refs.filter( - filterAuditsByTsOptions(definitive, opt?.onlyAudits), + filterAuditsByCompilerOptions(definitive, opt?.onlyAudits), ), })) .filter(group => group.refs.length > 0) @@ -113,34 +109,6 @@ export async function getCategoryRefsFromGroups( })); } -export async function getCurrentTsVersion(): Promise { - const { stdout } = await executeProcess({ - command: 'npx', - args: ['tsc', '--version'], - }); - return stdout.split(' ').slice(-1).join('').trim() as SemVerString; -} - -export async function loadDefaultTsConfig(version: SemVerString) { - const __dirname = new URL('.', import.meta.url).pathname; - const configPath = `${__dirname}default-ts-configs/${version}.ts`; - - try { - await access(configPath); - } catch { - throw new Error(`Could not find default TS config for version ${version}.`); - } - - try { - const module = await import(configPath); - return module.default as { compilerOptions: CompilerOptions }; - } catch (error) { - throw new Error( - `Could load default TS config for version ${version}. /n ${(error as Error).message}`, - ); - } -} - /** * It will evaluate if the option strict is enabled. If so, it must enable all it's dependencies. * [Logic Reference](https://github.com/microsoft/TypeScript/blob/56a08250f3516b3f5bc120d6c7ab4450a9a69352/src/compiler/utilities.ts#L9262) @@ -162,64 +130,33 @@ export function handleCompilerOptionStrict(options: CompilerOptions) { }; } -// eslint-disable-next-line functional/no-let -let _COMPILER_OPTIONS: CompilerOptions; - /** * It will from the options, and the TS Version, get a final compiler options to be used later for filters * Once it's processed for the first time, it will store the information in a variable, to be retrieve * later if existing * @param options Plugin options */ -export async function getCompilerOptionsToDetermineListedAudits( +export async function normalizeCompilerOptions( options?: TypescriptPluginOptions, ) { - if (_COMPILER_OPTIONS) { - return _COMPILER_OPTIONS; - } const { tsConfigPath = DEFAULT_TS_CONFIG } = options ?? {}; - const { compilerOptions: defaultCompilerOptions } = await loadDefaultTsConfig( - await getCurrentTsVersion(), - ); + const { compilerOptions: defaultCompilerOptions } = + await loadTsConfigDefaultsByVersion(await getCurrentTsVersion()); const config = await loadTargetConfig(tsConfigPath); - const definitiveCompilerOptions = handleCompilerOptionStrict({ + return handleCompilerOptionStrict({ ...defaultCompilerOptions, ...config.options, }); - _COMPILER_OPTIONS = definitiveCompilerOptions; - return _COMPILER_OPTIONS; } -// used in presets -export async function getFinalAuditSlugs(options: TypescriptPluginOptions) { - const definitive = await getCompilerOptionsToDetermineListedAudits(options); - return Object.keys(definitive).map( - key => camelCaseToKebabCase(key) as AuditSlug, - ); -} - -const _TS_CONFIG_MAP = new Map(); - -export async function loadTargetConfig(tsConfigPath: string) { - if (_TS_CONFIG_MAP.get(tsConfigPath) === undefined) { - const { config } = parseConfigFileTextToJson( - tsConfigPath, - await readTextFile(tsConfigPath), - ); +export function validateAudits(filteredAudits: Audit[]) { + const skippedAudits = AUDITS.filter( + audit => !filteredAudits.some(filtered => filtered.slug === audit.slug), + ).map(audit => kebabCaseToCamelCase(audit.slug)); - const parsedConfig = parseJsonConfigFileContent( - config, - sys, - dirname(tsConfigPath), + if (skippedAudits.length > 0) { + console.warn( + `Some audits were skipped because the configuration of the compiler options [${skippedAudits.join(', ')}]`, ); - - if (parsedConfig.fileNames.length === 0) { - throw new Error( - 'No files matched by the TypeScript configuration. Check your "include", "exclude" or "files" settings.', - ); - } - - _TS_CONFIG_MAP.set(tsConfigPath, parsedConfig); } - return _TS_CONFIG_MAP.get(tsConfigPath) as ParsedCommandLine; } diff --git a/packages/plugin-typescript/src/lib/utils.unit.test.ts b/packages/plugin-typescript/src/lib/utils.unit.test.ts index 99611c7be..98aabab9f 100644 --- a/packages/plugin-typescript/src/lib/utils.unit.test.ts +++ b/packages/plugin-typescript/src/lib/utils.unit.test.ts @@ -1,11 +1,7 @@ import type { CompilerOptions } from 'typescript'; import { describe, expect, it } from 'vitest'; import type { Audit, Group } from '@code-pushup/models'; -import { - filterAuditsBySlug, - filterGroupsByAuditSlug, - handleCompilerOptionStrict, -} from './utils.js'; +import { filterAuditsBySlug, handleCompilerOptionStrict } from './utils.js'; describe('filterAuditsBySlug', () => { const mockAudits: Audit[] = [ @@ -28,7 +24,7 @@ describe('filterAuditsBySlug', () => { }, ); }); - +/* describe('filterGroupsByAuditSlug', () => { const mockGroups: Group[] = [ { @@ -57,7 +53,7 @@ describe('filterGroupsByAuditSlug', () => { it.each(mockGroups)( 'should return true for group %# when no slugs provided', group => { - const filter = filterGroupsByAuditSlug(); + const filter = filterGr(); expect(filter(group)).toBe(true); }, ); @@ -79,7 +75,7 @@ describe('filterGroupsByAuditSlug', () => { expect(filter(group!)).toBe(expected); }); }); - +*/ describe('handleCompilerOptionStrict', () => { it('should return original options when strict is false', () => { const options: CompilerOptions = { diff --git a/packages/plugin-typescript/tools/generate-ts-config.ts b/packages/plugin-typescript/tools/generate-ts-config.ts index 12a0e8af7..c6ec15404 100644 --- a/packages/plugin-typescript/tools/generate-ts-config.ts +++ b/packages/plugin-typescript/tools/generate-ts-config.ts @@ -25,7 +25,7 @@ import { basename, join } from 'node:path'; import * as process from 'node:process'; import type { CompilerOptions } from 'typescript'; import { readTextFile } from '@code-pushup/utils'; -import type { SemVerString } from '../src/lib/types.js'; +import type { SemVerString } from '../src/lib/runner/types'; export const TS_CONFIG_DIR = join( 'packages', diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 28b4328a8..92067e711 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -97,6 +97,7 @@ export { export { isSemver, normalizeSemver, sortSemvers } from './lib/semver.js'; export { camelCaseToKebabCase, + type CamelCaseToKebabCase, kebabCaseToSentence, kebabCaseToCamelCase, } from './lib/string.js'; diff --git a/packages/utils/src/lib/string.ts b/packages/utils/src/lib/string.ts index 42741354a..4602d6f0f 100644 --- a/packages/utils/src/lib/string.ts +++ b/packages/utils/src/lib/string.ts @@ -14,17 +14,27 @@ export function kebabCaseToCamelCase(string: string) { .join(''); } +export type CamelCaseToKebabCase = + T extends `${infer First}${infer Rest}` + ? Rest extends Uncapitalize + ? `${Lowercase}${CamelCaseToKebabCase}` + : `${Lowercase}-${CamelCaseToKebabCase}` + : T; + /** * Converts a camelCase string to kebab-case. * @param string - The camelCase string to convert. * @returns The kebab-case string. */ -export function camelCaseToKebabCase(string: string): string { +export function camelCaseToKebabCase( + string: T, +): CamelCaseToKebabCase { return string - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // handle consecutive capital letters - .replace(/([a-z])([A-Z])/g, '$1-$2') - .replace(/[\s_]+/g, '-') - .toLowerCase(); + .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; } /**