diff --git a/eslint.config.js b/eslint.config.js index c25afb9dc..cf20609ed 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,6 +28,7 @@ export default tseslint.config( String.raw`^.*/eslint(\.base)?\.config\.[cm]?js$`, String.raw`^.*/code-pushup\.(config|preset)(\.m?[jt]s)?$`, '^[./]+/tools/.*$', + String.raw`^[./]+/(testing/)?test-setup-config/src/index\.js$`, ], depConstraints: [ { @@ -115,6 +116,7 @@ export default tseslint.config( files: ['**/*.ts', '**/*.js'], rules: { 'n/file-extension-in-import': ['error', 'always'], + 'unicorn/number-literal-case': 'off', }, }, { @@ -127,7 +129,7 @@ export default tseslint.config( { // tests need only be compatible with local Node version // publishable packages should pick up version range from "engines" in their package.json - files: ['e2e/**/*.ts', 'testing/**/*.ts'], + files: ['e2e/**/*.ts', 'testing/**/*.ts', '**/*.test.ts'], settings: { node: { version: fs.readFileSync('.node-version', 'utf8'), diff --git a/nx.json b/nx.json index 4f9f515b5..80aa82594 100644 --- a/nx.json +++ b/nx.json @@ -72,7 +72,7 @@ "options": { "command": "eslint", "args": [ - "{projectRoot}/**/*.ts", + "'{projectRoot}/**/*.ts'", "{projectRoot}/package.json", "--config={projectRoot}/eslint.config.js", "--max-warnings=0", diff --git a/packages/ci/vitest.int.config.ts b/packages/ci/vitest.int.config.ts index 6e5eecf6d..7461ff1fc 100644 --- a/packages/ci/vitest.int.config.ts +++ b/packages/ci/vitest.int.config.ts @@ -1,16 +1,3 @@ import { createIntTestConfig } from '../../testing/test-setup-config/src/index.js'; -let config = createIntTestConfig('ci'); - -config = { - ...config, - test: { - ...config.test, - setupFiles: [ - ...(config.test!.setupFiles || []), - '../../testing/test-setup/src/lib/logger.mock.ts', - ], - }, -}; - -export default config; +export default createIntTestConfig('ci'); diff --git a/packages/ci/vitest.unit.config.ts b/packages/ci/vitest.unit.config.ts index be6aa8cb1..38940076f 100644 --- a/packages/ci/vitest.unit.config.ts +++ b/packages/ci/vitest.unit.config.ts @@ -1,16 +1,3 @@ import { createUnitTestConfig } from '../../testing/test-setup-config/src/index.js'; -let config = createUnitTestConfig('ci'); - -config = { - ...config, - test: { - ...config.test, - setupFiles: [ - ...(config.test!.setupFiles || []), - '../../testing/test-setup/src/lib/logger.mock.ts', - ], - }, -}; - -export default config; +export default createUnitTestConfig('ci'); diff --git a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts index 9762be94a..dcb2b7582 100644 --- a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts +++ b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts @@ -24,7 +24,7 @@ describe('executePlugin', () => { ReturnType<(typeof runnerModule)['executePluginRunner']> >; - beforeAll(() => { + beforeEach(() => { readRunnerResultsSpy = vi.spyOn(runnerModule, 'readRunnerResults'); executePluginRunnerSpy = vi.spyOn(runnerModule, 'executePluginRunner'); }); @@ -35,11 +35,6 @@ describe('executePlugin', () => { }); it('should execute a valid plugin config and pass runner params', async () => { - const executePluginRunnerSpy = vi.spyOn( - runnerModule, - 'executePluginRunner', - ); - await expect( executePlugin(MINIMAL_PLUGIN_CONFIG_MOCK, { persist: {}, @@ -68,8 +63,6 @@ describe('executePlugin', () => { }); it('should try to read cache if cache.read is true', async () => { - const readRunnerResultsSpy = vi.spyOn(runnerModule, 'readRunnerResults'); - const validRunnerResult = { duration: 0, // readRunnerResults now automatically sets this to 0 for cache hits date: new Date().toISOString(), // readRunnerResults sets this to current time @@ -106,12 +99,6 @@ describe('executePlugin', () => { }); it('should try to execute runner if cache.read is true and file not present', async () => { - const readRunnerResultsSpy = vi.spyOn(runnerModule, 'readRunnerResults'); - const executePluginRunnerSpy = vi.spyOn( - runnerModule, - 'executePluginRunner', - ); - readRunnerResultsSpy.mockResolvedValue(null); const runnerResult = { duration: 1000, diff --git a/packages/core/src/lib/implementation/runner.ts b/packages/core/src/lib/implementation/runner.ts index 83c807f61..c1b125859 100644 --- a/packages/core/src/lib/implementation/runner.ts +++ b/packages/core/src/lib/implementation/runner.ts @@ -45,9 +45,7 @@ export async function executeRunnerConfig( await removeDirectoryIfExists(path.dirname(outputFile)); // transform unknownAuditOutputs to auditOutputs - const audits = outputTransform ? await outputTransform(outputs) : outputs; - - return audits; + return outputTransform ? await outputTransform(outputs) : outputs; } export async function executeRunnerFunction( @@ -55,8 +53,7 @@ export async function executeRunnerFunction( args: RunnerArgs, ): Promise { // execute plugin runner - const audits = await runner(args); - return audits; + return runner(args); } /** diff --git a/packages/models/src/lib/implementation/validate.ts b/packages/models/src/lib/implementation/validate.ts index 97983b82c..ff380a2d0 100644 --- a/packages/models/src/lib/implementation/validate.ts +++ b/packages/models/src/lib/implementation/validate.ts @@ -11,6 +11,7 @@ type SchemaValidationContext = { */ type ZodInputLooseAutocomplete = | z.input + // eslint-disable-next-line @typescript-eslint/no-empty-object-type | {} | null | undefined; diff --git a/packages/models/src/lib/implementation/validate.unit.test.ts b/packages/models/src/lib/implementation/validate.unit.test.ts index 6495e3800..418c59a5a 100644 --- a/packages/models/src/lib/implementation/validate.unit.test.ts +++ b/packages/models/src/lib/implementation/validate.unit.test.ts @@ -15,7 +15,7 @@ describe('validate', () => { afterEach(async () => { // Allow any lingering async operations from transforms to complete // This prevents unhandled rejections in subsequent tests - await new Promise(resolve => setImmediate(resolve)); + await new Promise(setImmediate); }); it('should return parsed data if valid', () => { diff --git a/packages/models/src/lib/plugin-config.unit.test.ts b/packages/models/src/lib/plugin-config.unit.test.ts index 80eee1d2c..4ff7de38a 100644 --- a/packages/models/src/lib/plugin-config.unit.test.ts +++ b/packages/models/src/lib/plugin-config.unit.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; +import { ZodError } from 'zod'; import { type PluginConfig, pluginConfigSchema, @@ -164,28 +165,28 @@ describe('pluginUrlsSchema', () => { }); it('should throw for invalid URL', () => { - expect(() => pluginUrlsSchema.parse('invalid')).toThrow(); + expect(() => pluginUrlsSchema.parse('invalid')).toThrow(ZodError); }); it('should throw for array with invalid URL', () => { expect(() => pluginUrlsSchema.parse(['https://example.com', 'invalid']), - ).toThrow(); + ).toThrow(ZodError); }); it('should throw for object with invalid URL', () => { - expect(() => pluginUrlsSchema.parse({ invalid: 1 })).toThrow(); + expect(() => pluginUrlsSchema.parse({ invalid: 1 })).toThrow(ZodError); }); it('should throw for invalid negative weight', () => { - expect(() => - pluginUrlsSchema.parse({ 'https://example.com': -1 }), - ).toThrow(); + expect(() => pluginUrlsSchema.parse({ 'https://example.com': -1 })).toThrow( + ZodError, + ); }); it('should throw for invalid string weight', () => { expect(() => pluginUrlsSchema.parse({ 'https://example.com': '1' }), - ).toThrow(); + ).toThrow(ZodError); }); }); diff --git a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts index c5684b0ba..bcac3e153 100644 --- a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts +++ b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts @@ -16,11 +16,13 @@ describe('runAutorunExecutor', () => { beforeAll(() => { Object.entries(process.env) .filter(([k]) => k.startsWith('CP_')) - .forEach(([k]) => delete process.env[k]); + .forEach(([k]) => Reflect.deleteProperty(process.env, k)); }); afterAll(() => { - Object.entries(processEnvCP).forEach(([k, v]) => (process.env[k] = v)); + Object.entries(processEnvCP).forEach(([k, v]) => + Reflect.set(process.env, k, v), + ); }); beforeEach(() => { diff --git a/packages/nx-plugin/src/executors/internal/cli.ts b/packages/nx-plugin/src/executors/internal/cli.ts index adbf1627c..6ae34f9c7 100644 --- a/packages/nx-plugin/src/executors/internal/cli.ts +++ b/packages/nx-plugin/src/executors/internal/cli.ts @@ -1,5 +1,3 @@ -import { logger } from '@nx/devkit'; - export function createCliCommandString(options?: { args?: Record; command?: string; diff --git a/packages/nx-plugin/src/generators/configuration/generator.int.test.ts b/packages/nx-plugin/src/generators/configuration/generator.int.test.ts index 25c6cbae2..ba779c955 100644 --- a/packages/nx-plugin/src/generators/configuration/generator.int.test.ts +++ b/packages/nx-plugin/src/generators/configuration/generator.int.test.ts @@ -7,7 +7,6 @@ import { import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import * as path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_TARGET_NAME, PACKAGE_NAME } from '../../internal/constants.js'; import { configurationGenerator } from './generator.js'; describe('configurationGenerator', () => { diff --git a/packages/nx-plugin/src/internal/execute-process.ts b/packages/nx-plugin/src/internal/execute-process.ts index 9f8f68a05..26a2d3ebf 100644 --- a/packages/nx-plugin/src/internal/execute-process.ts +++ b/packages/nx-plugin/src/internal/execute-process.ts @@ -6,6 +6,8 @@ export async function executeProcess( cfg: import('@code-pushup/utils').ProcessConfig, ): Promise { - const { executeProcess } = await import('@code-pushup/utils'); - return executeProcess(cfg); + const { executeProcess: executeProcessFromUtils } = await import( + '@code-pushup/utils' + ); + return executeProcessFromUtils(cfg); } diff --git a/packages/nx-plugin/src/plugin/target/configuration-target.ts b/packages/nx-plugin/src/plugin/target/configuration-target.ts index cc9655969..64a526c79 100644 --- a/packages/nx-plugin/src/plugin/target/configuration-target.ts +++ b/packages/nx-plugin/src/plugin/target/configuration-target.ts @@ -11,7 +11,9 @@ export function createConfigurationTarget(options?: { const args = objectToCliArgs({ ...(projectName ? { project: projectName } : {}), }); + const argsString = args.length > 0 ? args.join(' ') : ''; + const baseCommand = `nx g ${bin}:configuration`; return { - command: `nx g ${bin}:configuration${args.length > 0 ? ` ${args.join(' ')}` : ''}`, + command: argsString ? `${baseCommand} ${argsString}` : baseCommand, }; } diff --git a/packages/plugin-axe/src/lib/config.ts b/packages/plugin-axe/src/lib/config.ts index 6d45cfdfa..a5f0f6aea 100644 --- a/packages/plugin-axe/src/lib/config.ts +++ b/packages/plugin-axe/src/lib/config.ts @@ -5,6 +5,8 @@ import { } from '@code-pushup/models'; import { AXE_DEFAULT_PRESET, AXE_PRESETS } from './constants.js'; +const DEFAULT_TIMEOUT_MS = 30_000; + export const axePluginOptionsSchema = z .object({ preset: z.enum(AXE_PRESETS).default(AXE_DEFAULT_PRESET).meta({ @@ -12,7 +14,7 @@ export const axePluginOptionsSchema = z 'Accessibility ruleset preset (default: wcag21aa for WCAG 2.1 Level AA compliance)', }), scoreTargets: pluginScoreTargetsSchema.optional(), - timeout: positiveIntSchema.default(30_000).meta({ + timeout: positiveIntSchema.default(DEFAULT_TIMEOUT_MS).meta({ description: 'Page navigation timeout in milliseconds (default: 30000ms / 30s)', }), diff --git a/packages/plugin-axe/src/lib/config.unit.test.ts b/packages/plugin-axe/src/lib/config.unit.test.ts index 0e4e90dd0..6eb467e8c 100644 --- a/packages/plugin-axe/src/lib/config.unit.test.ts +++ b/packages/plugin-axe/src/lib/config.unit.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; +import { ZodError } from 'zod'; import { axePluginOptionsSchema } from './config.js'; describe('axePluginOptionsSchema', () => { @@ -31,17 +32,21 @@ describe('axePluginOptionsSchema', () => { }); it('should reject invalid preset values', () => { - expect(() => axePluginOptionsSchema.parse({ preset: 'wcag3aa' })).toThrow(); + expect(() => axePluginOptionsSchema.parse({ preset: 'wcag3aa' })).toThrow( + ZodError, + ); }); it('should reject scoreTargets values greater than 1', () => { - expect(() => axePluginOptionsSchema.parse({ scoreTargets: 1.5 })).toThrow(); + expect(() => axePluginOptionsSchema.parse({ scoreTargets: 1.5 })).toThrow( + ZodError, + ); }); it('should reject negative scoreTargets', () => { - expect(() => - axePluginOptionsSchema.parse({ scoreTargets: -0.1 }), - ).toThrow(); + expect(() => axePluginOptionsSchema.parse({ scoreTargets: -0.1 })).toThrow( + ZodError, + ); }); it('should accept custom timeout value', () => { @@ -51,11 +56,17 @@ describe('axePluginOptionsSchema', () => { }); it('should reject non-positive timeout values', () => { - expect(() => axePluginOptionsSchema.parse({ timeout: 0 })).toThrow(); - expect(() => axePluginOptionsSchema.parse({ timeout: -1000 })).toThrow(); + expect(() => axePluginOptionsSchema.parse({ timeout: 0 })).toThrow( + ZodError, + ); + expect(() => axePluginOptionsSchema.parse({ timeout: -1000 })).toThrow( + ZodError, + ); }); it('should reject non-integer timeout values', () => { - expect(() => axePluginOptionsSchema.parse({ timeout: 1000.5 })).toThrow(); + expect(() => axePluginOptionsSchema.parse({ timeout: 1000.5 })).toThrow( + ZodError, + ); }); }); diff --git a/packages/plugin-axe/src/lib/meta/groups.unit.test.ts b/packages/plugin-axe/src/lib/meta/groups.unit.test.ts index c2015a95d..00b0c5851 100644 --- a/packages/plugin-axe/src/lib/meta/groups.unit.test.ts +++ b/packages/plugin-axe/src/lib/meta/groups.unit.test.ts @@ -77,7 +77,7 @@ describe('transformRulesToGroups', () => { 'best-practice', ); - expect(groups).toSatisfyAll(({ slug }) => !slug.match(/^cat\./)); + expect(groups).toSatisfyAll(({ slug }) => !/^cat\./.test(slug)); }); it('should include both WCAG 2.2 and category groups for "all" preset', () => { diff --git a/packages/plugin-axe/src/lib/meta/transform.ts b/packages/plugin-axe/src/lib/meta/transform.ts index 516952fab..9c4401ffa 100644 --- a/packages/plugin-axe/src/lib/meta/transform.ts +++ b/packages/plugin-axe/src/lib/meta/transform.ts @@ -47,6 +47,7 @@ export function transformRulesToGroups( return createWcagGroups(rules, '2.1'); case 'wcag22aa': return createWcagGroups(rules, '2.2'); + // eslint-disable-next-line sonarjs/no-duplicate-string case 'best-practice': return createCategoryGroups(rules); case 'all': @@ -128,7 +129,7 @@ function createCategoryGroups(rules: axe.RuleMetadata[]): Group[] { rules.flatMap(({ tags }) => tags.filter(tag => tag.startsWith('cat.'))), ); - return Array.from(categoryTags).map(tag => { + return [...categoryTags].map(tag => { const slug = tag.replace('cat.', ''); const title = formatCategoryTitle(tag, slug); const categoryRules = rules.filter(({ tags }) => tags.includes(tag)); diff --git a/packages/plugin-axe/src/lib/processing.unit.test.ts b/packages/plugin-axe/src/lib/processing.unit.test.ts index ac448df3a..5fd66d810 100644 --- a/packages/plugin-axe/src/lib/processing.unit.test.ts +++ b/packages/plugin-axe/src/lib/processing.unit.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { processAuditsAndGroups } from './processing'; +import { processAuditsAndGroups } from './processing.js'; describe('processAuditsAndGroups', () => { it('should return audits and groups without expansion when analyzing single URL', () => { diff --git a/packages/plugin-axe/src/lib/runner/transform.ts b/packages/plugin-axe/src/lib/runner/transform.ts index 9fabaebbb..017d532b8 100644 --- a/packages/plugin-axe/src/lib/runner/transform.ts +++ b/packages/plugin-axe/src/lib/runner/transform.ts @@ -1,5 +1,4 @@ -import type { AxeResults, ImpactValue, NodeResult, Result } from 'axe-core'; -import type axe from 'axe-core'; +import axe from 'axe-core'; import type { AuditOutput, AuditOutputs, @@ -18,7 +17,7 @@ import { * Priority: violations > incomplete > passes > inapplicable */ export function toAuditOutputs( - { passes, violations, incomplete, inapplicable }: AxeResults, + { passes, violations, incomplete, inapplicable }: axe.AxeResults, url: string, ): AuditOutputs { const auditMap = new Map([ @@ -28,7 +27,7 @@ export function toAuditOutputs( ...violations.map(res => [res.id, toAuditOutput(res, url, 0)] as const), ]); - return Array.from(auditMap.values()); + return [...auditMap.values()]; } /** @@ -36,7 +35,7 @@ export function toAuditOutputs( * For passing audits (score 1), only includes element count. */ function toAuditOutput( - result: Result, + result: axe.Result, url: string, score: number, ): AuditOutput { @@ -69,7 +68,7 @@ function formatSelector(selector: axe.CrossTreeSelector): string { return selector.join(' >> '); } -function toIssue(node: NodeResult, result: Result, url: string): Issue { +function toIssue(node: axe.NodeResult, result: axe.Result, url: string): Issue { const selector = formatSelector(node.target?.[0] || node.html); const rawMessage = node.failureSummary || result.help; const cleanedMessage = rawMessage.replace(/\s+/g, ' ').trim(); @@ -82,7 +81,7 @@ function toIssue(node: NodeResult, result: Result, url: string): Issue { }; } -function impactToSeverity(impact: ImpactValue | undefined): IssueSeverity { +function impactToSeverity(impact: axe.ImpactValue | undefined): IssueSeverity { switch (impact) { case 'critical': case 'serious': diff --git a/packages/plugin-axe/src/lib/runner/transform.unit.test.ts b/packages/plugin-axe/src/lib/runner/transform.unit.test.ts index 948cd2594..39f83e274 100644 --- a/packages/plugin-axe/src/lib/runner/transform.unit.test.ts +++ b/packages/plugin-axe/src/lib/runner/transform.unit.test.ts @@ -287,7 +287,7 @@ describe('toAuditOutputs', () => { issues: [ { message: - '[`
Content
`] Fix this: Ensure all values assigned to role=\"\" correspond to valid ARIA roles ([example.com](https://example.com))', + '[`
Content
`] Fix this: Ensure all values assigned to role="" correspond to valid ARIA roles ([example.com](https://example.com))', severity: 'error', }, ], diff --git a/packages/plugin-coverage/src/lib/runner/index.ts b/packages/plugin-coverage/src/lib/runner/index.ts index 79dadcb1b..34801d8cb 100644 --- a/packages/plugin-coverage/src/lib/runner/index.ts +++ b/packages/plugin-coverage/src/lib/runner/index.ts @@ -24,7 +24,7 @@ export async function executeRunner({ const { command, args } = coverageToolCommand; try { await executeProcess({ command, args }); - } catch (error) { + } catch { if (!continueOnCommandFail) { throw new Error( 'Coverage plugin: Running coverage tool failed. Make sure all your provided tests are passing.', diff --git a/packages/plugin-eslint/src/lib/runner/transform.ts b/packages/plugin-eslint/src/lib/runner/transform.ts index c0ab6b7b4..67e273fe7 100644 --- a/packages/plugin-eslint/src/lib/runner/transform.ts +++ b/packages/plugin-eslint/src/lib/runner/transform.ts @@ -1,12 +1,8 @@ import type { Linter } from 'eslint'; import type { AuditOutput, Issue, IssueSeverity } from '@code-pushup/models'; import { - compareIssueSeverity, - countOccurrences, formatIssueSeverities, logger, - objectToEntries, - pluralizeToken, truncateIssueMessage, } from '@code-pushup/utils'; import { ruleIdToSlug } from '../meta/index.js'; diff --git a/packages/plugin-lighthouse/src/lib/processing.ts b/packages/plugin-lighthouse/src/lib/processing.ts index 089c6ab77..f84a79dc8 100644 --- a/packages/plugin-lighthouse/src/lib/processing.ts +++ b/packages/plugin-lighthouse/src/lib/processing.ts @@ -1,4 +1,3 @@ -import type { Audit, Group } from '@code-pushup/models'; import { addIndex, expandAuditsForUrls, diff --git a/packages/plugin-lighthouse/src/lib/runner/utils.ts b/packages/plugin-lighthouse/src/lib/runner/utils.ts index 37881eea5..50a4ed47e 100644 --- a/packages/plugin-lighthouse/src/lib/runner/utils.ts +++ b/packages/plugin-lighthouse/src/lib/runner/utils.ts @@ -190,6 +190,7 @@ export function withLocalTmpDir(fn: () => Promise): () => Promise { return async () => { const originalTmpDir = process.env['TEMP']; + // eslint-disable-next-line functional/immutable-data process.env['TEMP'] = path.join( pluginWorkDir(LIGHTHOUSE_PLUGIN_SLUG), 'tmp', @@ -198,6 +199,7 @@ export function withLocalTmpDir(fn: () => Promise): () => Promise { try { return await fn(); } finally { + // eslint-disable-next-line functional/immutable-data process.env['TEMP'] = originalTmpDir; } }; diff --git a/packages/utils/src/lib/execute-process.int.test.ts b/packages/utils/src/lib/execute-process.int.test.ts index 8c7e5d97c..8cde47dfd 100644 --- a/packages/utils/src/lib/execute-process.int.test.ts +++ b/packages/utils/src/lib/execute-process.int.test.ts @@ -130,7 +130,7 @@ process:complete throwError: true, }), ), - ).rejects.toThrow(); + ).rejects.toThrow('Process failed with exit code 1'); expect(logger.debug).toHaveBeenCalledWith( expect.stringMatching(/process:start.*Error: dummy-error/s), { force: true }, diff --git a/packages/utils/src/lib/file-system.ts b/packages/utils/src/lib/file-system.ts index d9c7deb2a..5dacec734 100644 --- a/packages/utils/src/lib/file-system.ts +++ b/packages/utils/src/lib/file-system.ts @@ -67,7 +67,7 @@ export function logMultipleFileResults( return `- ${ansis.bold(fileName)}${formattedSize}`; }; const failedTransform = (result: PromiseRejectedResult) => - `- ${ansis.bold(`${result.reason}`)}`; + `- ${ansis.bold(String(result.reason))}`; logMultipleResults( fileResults, diff --git a/packages/utils/src/lib/formatting.ts b/packages/utils/src/lib/formatting.ts index 5456b3f59..1a7eaec64 100644 --- a/packages/utils/src/lib/formatting.ts +++ b/packages/utils/src/lib/formatting.ts @@ -131,7 +131,7 @@ export function truncateMultilineText( const crlfIndex = text.indexOf('\r\n'); const lfIndex = text.indexOf('\n'); - const index = crlfIndex >= 0 ? crlfIndex : lfIndex; + const index = crlfIndex === -1 ? lfIndex : crlfIndex; if (index < 0) { return text; diff --git a/packages/utils/src/lib/formatting.unit.test.ts b/packages/utils/src/lib/formatting.unit.test.ts index cfa384e61..95ba97502 100644 --- a/packages/utils/src/lib/formatting.unit.test.ts +++ b/packages/utils/src/lib/formatting.unit.test.ts @@ -24,7 +24,7 @@ describe('roundDecimals', () => { }); it('should return number to prevent unnecessary trailing 0s in decimals', () => { - const result = roundDecimals(42.500001, 3); + const result = roundDecimals(42.500_001, 3); expect(result).toBeTypeOf('number'); expect(result.toString()).toBe('42.5'); expect(result.toString()).not.toBe('42.50'); @@ -113,7 +113,7 @@ describe('formatDuration', () => { [891, '891 ms'], [499.85, '500 ms'], [1200, '1.2 s'], - [56789, '56.79 s'], + [56_789, '56.79 s'], [60_000, '60 s'], ])('should format duration of %s milliseconds as %s', (ms, displayValue) => { expect(formatDuration(ms)).toBe(displayValue); @@ -212,7 +212,10 @@ describe('transformLines', () => { expect( transformLines( `export function greet(name = 'World') {\n console.log('Hello, ' + name + '!');\n}\n`, - line => `${ansis.gray(`${++count} | `)}${line}`, + line => { + const prefix = `${++count} | `; + return `${ansis.gray(prefix)}${line}`; + }, ), ).toBe( ` diff --git a/packages/utils/src/lib/logger.int.test.ts b/packages/utils/src/lib/logger.int.test.ts index e71ea5327..04b8e05c3 100644 --- a/packages/utils/src/lib/logger.int.test.ts +++ b/packages/utils/src/lib/logger.int.test.ts @@ -8,11 +8,11 @@ import { Logger } from './logger.js'; // customize ora options for test environment vi.mock('ora', async (): Promise => { - const exports = await vi.importActual('ora'); + const oraModule = await vi.importActual('ora'); return { - ...exports, + ...oraModule, default: options => { - const spinner = exports.default({ + const spinner = oraModule.default({ // skip cli-cursor package hideCursor: false, // skip is-interactive package @@ -35,13 +35,14 @@ vi.mock('ora', async (): Promise => { }); describe('Logger', () => { - let output = ''; + let output: string; let consoleLogSpy: MockInstance; let processStderrSpy: MockInstance<[], typeof process.stderr>; let performanceNowSpy: MockInstance<[], number>; let mathRandomSpy: MockInstance<[], number>; beforeAll(() => { + output = ''; vi.useFakeTimers(); consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(message => { @@ -61,7 +62,7 @@ describe('Logger', () => { moveCursor: () => true, clearLine: () => { const idx = output.lastIndexOf('\n'); - output = idx >= 0 ? output.substring(0, idx + 1) : ''; + output = idx === -1 ? '' : output.slice(0, Math.max(0, idx + 1)); return true; }, }; @@ -134,9 +135,9 @@ ${ansis.red('Failed to load config')} logger.setVerbose(true); - expect(logger.isVerbose()).toBe(true); + expect(logger.isVerbose()).toBeTrue(); expect(process.env['CP_VERBOSE']).toBe('true'); - expect(new Logger().isVerbose()).toBe(true); + expect(new Logger().isVerbose()).toBeTrue(); }); }); @@ -267,7 +268,7 @@ ${ansis.cyan('└')} ${ansis.green(`Total line coverage is ${ansis.bold('82%')}` it('should use collapsible sections in GitLab CI/CD environment, initial collapse depends on verbosity', async () => { vi.stubEnv('CI', 'true'); vi.stubEnv('GITLAB_CI', 'true'); - vi.setSystemTime(new Date(123456789000)); // current Unix timestamp: 123456789 seconds since epoch + vi.setSystemTime(new Date(123_456_789_000)); // current Unix timestamp: 123456789 seconds since epoch performanceNowSpy .mockReturnValueOnce(0) .mockReturnValueOnce(123) // 1st group duration: 123 ms @@ -292,16 +293,16 @@ ${ansis.cyan('└')} ${ansis.green(`Total line coverage is ${ansis.bold('82%')}` // debugging tip: temporarily remove '\r' character from original implementation expect(output).toBe(` -\x1b[0Ksection_start:123456789:code_pushup_logs_group_1a[collapsed=true]\r\x1b[0K${ansis.bold.cyan('❯ Running plugin "ESLint"')} +\u001B[0Ksection_start:123456789:code_pushup_logs_group_1a[collapsed=true]\r\u001B[0K${ansis.bold.cyan('❯ Running plugin "ESLint"')} ${ansis.cyan('│')} ${ansis.blue('$')} npx eslint . --format=json ${ansis.cyan('│')} ${ansis.yellow('Skipping unknown rule "deprecation/deprecation"')} ${ansis.cyan('└')} ${ansis.green('ESLint reported 4 errors and 11 warnings')} ${ansis.gray('(123 ms)')} -\x1b[0Ksection_end:123456789:code_pushup_logs_group_1a\r\x1b[0K +\u001B[0Ksection_end:123456789:code_pushup_logs_group_1a\r\u001B[0K -\x1b[0Ksection_start:123456789:code_pushup_logs_group_1b\r\x1b[0K${ansis.bold.magenta('❯ Running plugin "Code coverage"')} +\u001B[0Ksection_start:123456789:code_pushup_logs_group_1b\r\u001B[0K${ansis.bold.magenta('❯ Running plugin "Code coverage"')} ${ansis.magenta('│')} ${ansis.blue('$')} npx vitest --coverage.enabled ${ansis.magenta('└')} ${ansis.green(`Total line coverage is ${ansis.bold('82%')}`)} ${ansis.gray('(45 ms)')} -\x1b[0Ksection_end:123456789:code_pushup_logs_group_1b\r\x1b[0K +\u001B[0Ksection_end:123456789:code_pushup_logs_group_1b\r\u001B[0K `); }); @@ -373,7 +374,7 @@ ${ansis.green('✔')} Uploaded report to portal ${ansis.gray('(42 ms)')} .mockReturnValueOnce(0) .mockReturnValueOnce(30_000) // 1st task duration: 30 s .mockReturnValueOnce(0) - .mockReturnValueOnce(1_000); // 2nd task duration: 1 s + .mockReturnValueOnce(1000); // 2nd task duration: 1 s const task1 = new Logger().task( 'Collecting report', diff --git a/packages/utils/src/lib/logger.ts b/packages/utils/src/lib/logger.ts index 6e6bfe38d..151c76ea6 100644 --- a/packages/utils/src/lib/logger.ts +++ b/packages/utils/src/lib/logger.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines, no-console, @typescript-eslint/class-methods-use-this */ import ansis, { type AnsiColors } from 'ansis'; import os from 'node:os'; import path from 'node:path'; @@ -11,6 +12,14 @@ import { settlePromise } from './promises.js'; type GroupColor = Extract; type CiPlatform = 'GitHub Actions' | 'GitLab CI/CD'; +const HEX_RADIX = 16; + +const SIGINT_CODE = 2; +// https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html#:~:text=When%20a%20command%20terminates%20on%20a%20fatal%20signal%20whose%20number%20is%20N%2C%20Bash%20uses%20the%20value%20128%2BN%20as%20the%20exit%20status. +const SIGNALS_CODE_OFFSET_UNIX = 128; +const SIGINT_EXIT_CODE_UNIX = SIGNALS_CODE_OFFSET_UNIX + SIGINT_CODE; +const SIGINT_EXIT_CODE_WINDOWS = SIGINT_CODE; + /** * Rich logging implementation for Code PushUp CLI, plugins, etc. * @@ -53,7 +62,12 @@ export class Logger { } this.newline(); this.error(ansis.bold('Cancelled by SIGINT')); - process.exit(os.platform() === 'win32' ? 2 : 130); + // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit + process.exit( + os.platform() === 'win32' + ? SIGINT_EXIT_CODE_WINDOWS + : SIGINT_EXIT_CODE_UNIX, + ); }; /** @@ -186,7 +200,7 @@ export class Logger { await this.#spinner(worker, { pending: title, success: value => value, - failure: error => `${title} → ${ansis.red(`${error}`)}`, + failure: error => `${title} → ${ansis.red(String(error))}`, }); } @@ -246,6 +260,7 @@ export class Logger { * @param title Display title for the group. * @param worker Asynchronous implementation. Returned promise determines group status and ending message. Inner logs are attached to the group. */ + // eslint-disable-next-line max-lines-per-function async group( title: string, worker: () => Promise, @@ -329,8 +344,8 @@ export class Logger { }; case 'GitLab CI/CD': // https://docs.gitlab.com/ci/jobs/job_logs/#custom-collapsible-sections - const ansiEscCode = '\x1b[0K'; // '\e' ESC character only works for `echo -e`, Node console must use '\x1b' - const id = Math.random().toString(16).slice(2); + const ansiEscCode = '\u001B[0K'; // '\e' ESC character only works for `echo -e`, Node console must use '\u001B' + const id = Math.random().toString(HEX_RADIX).slice(2); const sectionId = `code_pushup_logs_group_${id}`; return { start: title => { @@ -360,6 +375,7 @@ export class Logger { return ansis.bold(this.#colorize(text, this.#groupColor)); } + // eslint-disable-next-line max-lines-per-function async #spinner( worker: () => Promise, messages: { diff --git a/packages/utils/src/lib/plugin-url-aggregation.unit.test.ts b/packages/utils/src/lib/plugin-url-aggregation.unit.test.ts index cfe241d20..6cd71d87c 100644 --- a/packages/utils/src/lib/plugin-url-aggregation.unit.test.ts +++ b/packages/utils/src/lib/plugin-url-aggregation.unit.test.ts @@ -9,7 +9,7 @@ import { resolveUrlWeight, shouldExpandForUrls, validateUrlContext, -} from './plugin-url-aggregation'; +} from './plugin-url-aggregation.js'; describe('shouldExpandForUrls', () => { it.each([ diff --git a/packages/utils/src/lib/plugin-url-config.unit.test.ts b/packages/utils/src/lib/plugin-url-config.unit.test.ts index 54c356953..c28374f6e 100644 --- a/packages/utils/src/lib/plugin-url-config.unit.test.ts +++ b/packages/utils/src/lib/plugin-url-config.unit.test.ts @@ -1,4 +1,4 @@ -import { getUrlIdentifier, normalizeUrlInput } from './plugin-url-config'; +import { getUrlIdentifier, normalizeUrlInput } from './plugin-url-config.js'; describe('getUrlIdentifier', () => { it.each([ diff --git a/packages/utils/src/lib/reports/formatting.ts b/packages/utils/src/lib/reports/formatting.ts index ed62ce599..4ecb7d0d0 100644 --- a/packages/utils/src/lib/reports/formatting.ts +++ b/packages/utils/src/lib/reports/formatting.ts @@ -210,5 +210,5 @@ export function wrapTags(text: string | undefined): string { if (!text) { return ''; } - return text.replace(/<[a-z][a-z0-9]*[^>]*>/gi, '`$&`'); + return text.replace(/<[a-z][a-z\d]*[^>]*>/gi, '`$&`'); } diff --git a/packages/utils/src/lib/text-formats/ascii/sticker.ts b/packages/utils/src/lib/text-formats/ascii/sticker.ts index fe619f23c..5e5255e32 100644 --- a/packages/utils/src/lib/text-formats/ascii/sticker.ts +++ b/packages/utils/src/lib/text-formats/ascii/sticker.ts @@ -1,8 +1,10 @@ import { formatAsciiTable } from './table.js'; +const STICKER_PADDING = 4; + export function formatAsciiSticker(lines: string[]): string { return formatAsciiTable( { rows: ['', ...lines, ''].map(line => [line]) }, - { padding: 4 }, + { padding: STICKER_PADDING }, ); } diff --git a/packages/utils/src/lib/text-formats/ascii/sticker.unit.test.ts b/packages/utils/src/lib/text-formats/ascii/sticker.unit.test.ts index 60f283199..5a4f28ee9 100644 --- a/packages/utils/src/lib/text-formats/ascii/sticker.unit.test.ts +++ b/packages/utils/src/lib/text-formats/ascii/sticker.unit.test.ts @@ -1,5 +1,5 @@ import ansis from 'ansis'; -import { formatAsciiSticker } from './sticker'; +import { formatAsciiSticker } from './sticker.js'; describe('formatAsciiSticker', () => { it('should frame lines with border and padding', () => { diff --git a/packages/utils/src/lib/text-formats/ascii/table.ts b/packages/utils/src/lib/text-formats/ascii/table.ts index 3d366631d..f8c888e7b 100644 --- a/packages/utils/src/lib/text-formats/ascii/table.ts +++ b/packages/utils/src/lib/text-formats/ascii/table.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import ansis from 'ansis'; import type { TableCellAlignment } from 'build-md'; import stringWidth from 'string-width'; @@ -272,20 +273,25 @@ function truncateColumns( (a, b) => b.maxWidth - a.maxWidth || b.maxWord.length - a.maxWord.length, ); - let remaining = overflow; - const newWidths = new Map(); - for (const { index, maxWidth, maxWord } of sortedColumns) { - const newWidth = Math.max( - maxWidth - remaining, - Math.ceil(maxWidth / 2), - Math.ceil(maxWord.length / 2) + 1, - ); - newWidths.set(index, newWidth); - remaining -= maxWidth - newWidth; - if (remaining <= 0) { - break; - } - } + const { newWidths } = sortedColumns.reduce( + (acc, { index, maxWidth, maxWord }) => { + if (acc.remaining <= 0) { + return acc; + } + const newWidth = Math.max( + maxWidth - acc.remaining, + Math.ceil(maxWidth / 2), + Math.ceil(maxWord.length / 2) + 1, + ); + const updatedWidths = new Map(acc.newWidths); + updatedWidths.set(index, newWidth); + return { + remaining: acc.remaining - (maxWidth - newWidth), + newWidths: updatedWidths, + }; + }, + { remaining: overflow, newWidths: new Map() }, + ); return columnStats.map( ({ maxWidth }, index) => newWidths.get(index) ?? maxWidth, ); @@ -313,7 +319,6 @@ function normalizeTableColumns( ): TableCell[] | undefined { if ( columns == null || - columns.length === 0 || columns.every(column => typeof column === 'string') || columns.every(column => !normalizeColumnTitle(column)) ) { diff --git a/testing/test-nx-utils/vitest.unit.config.ts b/testing/test-nx-utils/vitest.unit.config.ts index c7bd7a43a..3aba6012a 100644 --- a/testing/test-nx-utils/vitest.unit.config.ts +++ b/testing/test-nx-utils/vitest.unit.config.ts @@ -1,3 +1,3 @@ -import { createUnitTestConfig } from '../../testing/test-setup-config/src/index.js'; +import { createUnitTestConfig } from '../test-setup-config/src/index.js'; export default createUnitTestConfig('test-nx-utils'); diff --git a/testing/test-setup-config/project.json b/testing/test-setup-config/project.json index c4b32ba8a..21a9b7322 100644 --- a/testing/test-setup-config/project.json +++ b/testing/test-setup-config/project.json @@ -1,7 +1,7 @@ { "name": "test-setup-config", "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "testing/test-setup/src", + "sourceRoot": "testing/test-setup-config/src", "projectType": "library", "targets": { "build": {}, diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.ts b/testing/test-setup-config/src/lib/vitest-config-factory.ts index 5bf1ad41c..66e349ed1 100644 --- a/testing/test-setup-config/src/lib/vitest-config-factory.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.ts @@ -1,5 +1,5 @@ import type { CoverageOptions } from 'vitest'; -import { type UserConfig as ViteUserConfig } from 'vitest/config'; +import type { UserConfig as ViteUserConfig } from 'vitest/config'; import { getSetupFiles } from './vitest-setup-files.js'; import { tsconfigPathAliases } from './vitest-tsconfig-path-aliases.js'; @@ -37,7 +37,7 @@ function buildCoverageConfig( return { reporter: ['text', 'lcov'], reportsDirectory, - exclude: exclude, + exclude, }; } diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts index 8891d8e04..3afa4b1fa 100644 --- a/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it, vi } from 'vitest'; -import type { E2ETestOptions, TestKind } from './vitest-config-factory.js'; -import { createVitestConfig } from './vitest-config-factory.js'; +import { + type E2ETestOptions, + type TestKind, + createVitestConfig, +} from './vitest-config-factory.js'; vi.mock('./vitest-tsconfig-path-aliases.js', () => ({ tsconfigPathAliases: vi diff --git a/testing/test-setup-config/src/lib/vitest-setup-files.ts b/testing/test-setup-config/src/lib/vitest-setup-files.ts index 4e286d1df..1d735b2a9 100644 --- a/testing/test-setup-config/src/lib/vitest-setup-files.ts +++ b/testing/test-setup-config/src/lib/vitest-setup-files.ts @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/no-duplicate-string */ import type { TestKind } from './vitest-config-factory.js'; /** diff --git a/testing/test-setup-config/src/lib/vitest-setup-files.unit.test.ts b/testing/test-setup-config/src/lib/vitest-setup-files.unit.test.ts index dc663a5df..6b9ada51d 100644 --- a/testing/test-setup-config/src/lib/vitest-setup-files.unit.test.ts +++ b/testing/test-setup-config/src/lib/vitest-setup-files.unit.test.ts @@ -32,9 +32,9 @@ describe('getSetupFiles', () => { const intFiles = getSetupFiles('int'); const e2eFiles = getSetupFiles('e2e'); - expect(unitFiles.length).not.toBe(intFiles.length); - expect(intFiles.length).not.toBe(e2eFiles.length); - expect(unitFiles.length).not.toBe(e2eFiles.length); + expect(unitFiles).not.toHaveLength(intFiles.length); + expect(intFiles).not.toHaveLength(e2eFiles.length); + expect(unitFiles).not.toHaveLength(e2eFiles.length); }); it('should show hierarchy: unit has most, e2e has least', () => { diff --git a/testing/test-setup/vitest.unit.config.ts b/testing/test-setup/vitest.unit.config.ts index 3ed3fa745..c492e2413 100644 --- a/testing/test-setup/vitest.unit.config.ts +++ b/testing/test-setup/vitest.unit.config.ts @@ -1,3 +1,3 @@ -import { createUnitTestConfig } from '../../testing/test-setup-config/src/index.js'; +import { createUnitTestConfig } from '../test-setup-config/src/index.js'; export default createUnitTestConfig('test-setup'); diff --git a/testing/test-utils/src/lib/utils/test-folder-setup.ts b/testing/test-utils/src/lib/utils/test-folder-setup.ts index c6064292e..9c5facb02 100644 --- a/testing/test-utils/src/lib/utils/test-folder-setup.ts +++ b/testing/test-utils/src/lib/utils/test-folder-setup.ts @@ -57,7 +57,8 @@ export const NX_IGNORED_FILES_TO_RESTORE: string[] = [ */ export async function restoreNxIgnoredFiles(dir: string): Promise { const entries = await readdir(dir, { withFileTypes: true }); - for (const entry of entries) { + await entries.reduce(async (previousPromise, entry) => { + await previousPromise; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { await restoreNxIgnoredFiles(fullPath); @@ -67,5 +68,5 @@ export async function restoreNxIgnoredFiles(dir: string): Promise { ) { await rename(fullPath, path.join(dir, entry.name.slice(1))); } - } + }, Promise.resolve()); } diff --git a/testing/test-utils/src/lib/utils/test-folder-setup.unit.test.ts b/testing/test-utils/src/lib/utils/test-folder-setup.unit.test.ts index 58310bbb1..7f5968366 100644 --- a/testing/test-utils/src/lib/utils/test-folder-setup.unit.test.ts +++ b/testing/test-utils/src/lib/utils/test-folder-setup.unit.test.ts @@ -1,7 +1,7 @@ import ansis from 'ansis'; import { vol } from 'memfs'; import { describe, expect, it, vi } from 'vitest'; -import { MEMFS_VOLUME } from '@code-pushup/test-utils'; +import { MEMFS_VOLUME } from '../constants.js'; import { cleanTestFolder, restoreNxIgnoredFiles, @@ -119,7 +119,7 @@ describe('teardownTestFolder', () => { MEMFS_VOLUME, ); - await expect(teardownTestFolder('/tmp/unit')).resolves.toEqual(undefined); + await expect(teardownTestFolder('/tmp/unit')).resolves.toBeUndefined(); // memfs represents empty directories as null, so /tmp remains as null after deletion expect(vol.toJSON()).toStrictEqual({ @@ -136,9 +136,9 @@ describe('teardownTestFolder', () => { MEMFS_VOLUME, ); - await expect(teardownTestFolder('/tmp/unit/package.json')).resolves.toEqual( - undefined, - ); + await expect( + teardownTestFolder('/tmp/unit/package.json'), + ).resolves.toBeUndefined(); expect(vol.toJSON()).toStrictEqual({ '/tmp/unit': null, diff --git a/testing/test-utils/vitest.unit.config.ts b/testing/test-utils/vitest.unit.config.ts index 61d9b2a20..fb37e9074 100644 --- a/testing/test-utils/vitest.unit.config.ts +++ b/testing/test-utils/vitest.unit.config.ts @@ -1,3 +1,3 @@ -import { createUnitTestConfig } from '../../testing/test-setup-config/src/index.js'; +import { createUnitTestConfig } from '../test-setup-config/src/index.js'; export default createUnitTestConfig('test-utils');