diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index af4c6c88d..fb228198e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -24,19 +24,21 @@ export { reportToMd } from './lib/report-to-md'; export { reportToStdout } from './lib/report-to-stdout'; export { ScoredReport, scoreReport } from './lib/scoring'; export { - countOccurrences, - distinct, - objectToEntries, - objectToKeys, - pluralize, readJsonFile, readTextFile, - toArray, toUnixPath, ensureDirectoryExists, FileResult, MultipleFileResults, logMultipleFileResults, - slugify, -} from './lib/utils'; +} from './lib/file-system'; export { verboseUtils } from './lib/verbose-utils'; +export { + pluralize, + toArray, + objectToKeys, + objectToEntries, + countOccurrences, + distinct, + slugify, +} from './lib/transformation'; diff --git a/packages/utils/src/lib/utils.spec.ts b/packages/utils/src/lib/file-system.spec.ts similarity index 66% rename from packages/utils/src/lib/utils.spec.ts rename to packages/utils/src/lib/file-system.spec.ts index e09824b6a..12f3e8bcc 100644 --- a/packages/utils/src/lib/utils.spec.ts +++ b/packages/utils/src/lib/file-system.spec.ts @@ -5,15 +5,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { MEMFS_VOLUME } from '@code-pushup/models/testing'; import { mockConsole, unmockConsole } from '../../test/console.mock'; import { - countOccurrences, - distinct, ensureDirectoryExists, logMultipleFileResults, - pluralize, - slugify, - toArray, toUnixPath, -} from './utils'; +} from './file-system'; // Mock file system API's vi.mock('fs', async () => { @@ -27,48 +22,6 @@ vi.mock('fs/promises', async () => { const outputDir = MEMFS_VOLUME; -describe('slugify', () => { - it.each([ - ['Largest Contentful Paint', 'largest-contentful-paint'], - ['cumulative-layout-shift', 'cumulative-layout-shift'], - ['max-lines-200', 'max-lines-200'], - ['rxjs/finnish', 'rxjs-finnish'], - ['@typescript-eslint/no-explicit-any', 'typescript-eslint-no-explicit-any'], - ['Code PushUp ', 'code-pushup'], - ])('should transform "%s" to valid slug "%s"', (text, slug) => { - expect(slugify(text)).toBe(slug); - }); -}); - -describe('pluralize', () => { - it.each([ - ['warning', 'warnings'], - ['error', 'errors'], - ['category', 'categories'], - ['status', 'statuses'], - ])('should pluralize "%s" as "%s"', (singular, plural) => { - expect(pluralize(singular)).toBe(plural); - }); -}); - -describe('toArray', () => { - it('should transform non-array value into array with single value', () => { - expect(toArray('src/**/*.ts')).toEqual(['src/**/*.ts']); - }); - - it('should leave array value unchanged', () => { - expect(toArray(['*.ts', '*.js'])).toEqual(['*.ts', '*.js']); - }); -}); - -describe('countOccurrences', () => { - it('should return record with counts for each item', () => { - expect( - countOccurrences(['error', 'warning', 'error', 'error', 'warning']), - ).toEqual({ error: 3, warning: 2 }); - }); -}); - describe('toUnixPath', () => { it.each([ ['main.ts', 'main.ts'], @@ -91,24 +44,6 @@ describe('toUnixPath', () => { }); }); -describe('distinct', () => { - it('should remove duplicate strings from array', () => { - expect( - distinct([ - 'no-unused-vars', - 'no-invalid-regexp', - 'no-unused-vars', - 'no-invalid-regexp', - '@typescript-eslint/no-unused-vars', - ]), - ).toEqual([ - 'no-unused-vars', - 'no-invalid-regexp', - '@typescript-eslint/no-unused-vars', - ]); - }); -}); - describe('ensureDirectoryExists', () => { beforeEach(() => { vol.reset(); diff --git a/packages/utils/src/lib/utils.ts b/packages/utils/src/lib/file-system.ts similarity index 65% rename from packages/utils/src/lib/utils.ts rename to packages/utils/src/lib/file-system.ts index c3fa505d7..165fd77f9 100644 --- a/packages/utils/src/lib/utils.ts +++ b/packages/utils/src/lib/file-system.ts @@ -2,53 +2,6 @@ import chalk from 'chalk'; import { mkdir, readFile } from 'fs/promises'; import { formatBytes } from './report'; -// === Transform - -export function slugify(text: string): string { - return text - .trim() - .toLowerCase() - .replace(/\s+|\//g, '-') - .replace(/[^a-z0-9-]/g, ''); -} - -export function pluralize(text: string): string { - if (text.endsWith('y')) { - return text.slice(0, -1) + 'ies'; - } - if (text.endsWith('s')) { - return `${text}es`; - } - return `${text}s`; -} - -export function toArray(val: T | T[]): T[] { - return Array.isArray(val) ? val : [val]; -} - -export function objectToKeys(obj: T) { - return Object.keys(obj) as (keyof T)[]; -} - -export function objectToEntries(obj: T) { - return Object.entries(obj) as [keyof T, T[keyof T]][]; -} - -export function countOccurrences( - values: T[], -): Partial> { - return values.reduce>>( - (acc, value) => ({ ...acc, [value]: (acc[value] ?? 0) + 1 }), - {}, - ); -} - -export function distinct(array: T[]): T[] { - return Array.from(new Set(array)); -} - -// === Filesystem @TODO move to fs-utils.ts - export function toUnixPath( path: string, options?: { toRelative?: boolean }, diff --git a/packages/utils/src/lib/report-to-md.ts b/packages/utils/src/lib/report-to-md.ts index 3e082258f..ec3ca2b18 100644 --- a/packages/utils/src/lib/report-to-md.ts +++ b/packages/utils/src/lib/report-to-md.ts @@ -33,7 +33,7 @@ import { reportOverviewTableHeaders, } from './report'; import { EnrichedScoredAuditGroup, ScoredReport } from './scoring'; -import { slugify } from './utils'; +import { slugify } from './transformation'; export function reportToMd( report: ScoredReport, diff --git a/packages/utils/src/lib/report.ts b/packages/utils/src/lib/report.ts index 7167d3f37..78c15bbd8 100644 --- a/packages/utils/src/lib/report.ts +++ b/packages/utils/src/lib/report.ts @@ -7,13 +7,13 @@ import { Report, reportSchema, } from '@code-pushup/models'; -import { ScoredReport } from './scoring'; import { ensureDirectoryExists, - pluralize, readJsonFile, readTextFile, -} from './utils'; +} from './file-system'; +import { ScoredReport } from './scoring'; +import { pluralize } from './transformation'; export const FOOTER_PREFIX = 'Made with ❤️ by'; export const CODE_PUSHUP_DOMAIN = 'code-pushup.dev'; diff --git a/packages/utils/src/lib/transformation.spec.ts b/packages/utils/src/lib/transformation.spec.ts new file mode 100644 index 000000000..8b21eabb4 --- /dev/null +++ b/packages/utils/src/lib/transformation.spec.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'vitest'; +import { + countOccurrences, + distinct, + objectToEntries, + objectToKeys, + pluralize, + slugify, + toArray, +} from './transformation'; + +describe('slugify', () => { + it.each([ + ['Largest Contentful Paint', 'largest-contentful-paint'], + ['cumulative-layout-shift', 'cumulative-layout-shift'], + ['max-lines-200', 'max-lines-200'], + ['rxjs/finnish', 'rxjs-finnish'], + ['@typescript-eslint/no-explicit-any', 'typescript-eslint-no-explicit-any'], + ['Code PushUp ', 'code-pushup'], + ])('should transform "%s" to valid slug "%s"', (text, slug) => { + expect(slugify(text)).toBe(slug); + }); +}); + +describe('pluralize', () => { + it.each([ + ['warning', 'warnings'], + ['error', 'errors'], + ['category', 'categories'], + ['status', 'statuses'], + ])('should pluralize "%s" as "%s"', (singular, plural) => { + expect(pluralize(singular)).toBe(plural); + }); +}); + +describe('toArray', () => { + it('should transform non-array value into array with single value', () => { + expect(toArray('src/**/*.ts')).toEqual(['src/**/*.ts']); + }); + + it('should leave array value unchanged', () => { + expect(toArray(['*.ts', '*.js'])).toEqual(['*.ts', '*.js']); + }); +}); + +describe('objectToKeys', () => { + it('should transform object into array of keys', () => { + const keys: 'prop1'[] = objectToKeys({ prop1: 1 }); + expect(keys).toEqual(['prop1']); + }); + + it('should transform empty object into empty array', () => { + const keys: never[] = objectToKeys({}); + expect(keys).toEqual([]); + }); +}); + +describe('objectToEntries', () => { + it('should transform object into array of entries', () => { + const keys: ['prop1', number][] = objectToEntries({ prop1: 1 }); + expect(keys).toEqual([['prop1', 1]]); + }); + + it('should transform empty object into empty array', () => { + const keys: [never, never][] = objectToEntries({}); + expect(keys).toEqual([]); + }); +}); + +describe('countOccurrences', () => { + it('should return record with counts for each item', () => { + expect( + countOccurrences(['error', 'warning', 'error', 'error', 'warning']), + ).toEqual({ error: 3, warning: 2 }); + }); + + it('should return empty record for no matches', () => { + expect(countOccurrences([])).toEqual({}); + }); +}); + +describe('distinct', () => { + it('should remove duplicate strings from array', () => { + expect( + distinct([ + 'no-unused-vars', + 'no-invalid-regexp', + 'no-unused-vars', + 'no-invalid-regexp', + '@typescript-eslint/no-unused-vars', + ]), + ).toEqual([ + 'no-unused-vars', + 'no-invalid-regexp', + '@typescript-eslint/no-unused-vars', + ]); + }); +}); diff --git a/packages/utils/src/lib/transformation.ts b/packages/utils/src/lib/transformation.ts new file mode 100644 index 000000000..2ba6ec76c --- /dev/null +++ b/packages/utils/src/lib/transformation.ts @@ -0,0 +1,42 @@ +export function slugify(text: string): string { + return text + .trim() + .toLowerCase() + .replace(/\s+|\//g, '-') + .replace(/[^a-z0-9-]/g, ''); +} + +export function pluralize(text: string): string { + if (text.endsWith('y')) { + return text.slice(0, -1) + 'ies'; + } + if (text.endsWith('s')) { + return `${text}es`; + } + return `${text}s`; +} + +export function toArray(val: T | T[]): T[] { + return Array.isArray(val) ? val : [val]; +} + +export function objectToKeys(obj: T) { + return Object.keys(obj) as (keyof T)[]; +} + +export function objectToEntries(obj: T) { + return Object.entries(obj) as [keyof T, T[keyof T]][]; +} + +export function countOccurrences( + values: T[], +): Partial> { + return values.reduce>>( + (acc, value) => ({ ...acc, [value]: (acc[value] ?? 0) + 1 }), + {}, + ); +} + +export function distinct(array: T[]): T[] { + return Array.from(new Set(array)); +}