From a07a99d776c24467b5fdc26445fbd1829af220dc Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 26 Jul 2025 07:47:26 +0200 Subject: [PATCH 01/36] refactor(core): add reporter options to config --- packages/core/src/lib/config/default-config.ts | 2 ++ packages/core/src/lib/config/user-sheriff-config.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/core/src/lib/config/default-config.ts b/packages/core/src/lib/config/default-config.ts index edb3c191..7f96b9ff 100644 --- a/packages/core/src/lib/config/default-config.ts +++ b/packages/core/src/lib/config/default-config.ts @@ -13,4 +13,6 @@ export const defaultConfig: Configuration = { isConfigFileMissing: false, barrelFileName: 'index.ts', entryPoints: undefined, + reportersDirectory: '.sheriff/reports', + defaultReporters: [], }; diff --git a/packages/core/src/lib/config/user-sheriff-config.ts b/packages/core/src/lib/config/user-sheriff-config.ts index 1764fd84..3ee92d8a 100644 --- a/packages/core/src/lib/config/user-sheriff-config.ts +++ b/packages/core/src/lib/config/user-sheriff-config.ts @@ -260,4 +260,14 @@ export interface UserSheriffConfig { * Either `entryFile` or `entryPoints` can be set, but not both. */ entryPoints?: Record; + /** + * The directory where the Sheriff CLI will write reports to. + * + * Default is `./sheriff/reports` + */ + reportersDirectory?: string; + /** + * The default reporters used to generate reports. + */ + defaultReporters?: string[]; } From 403569f073fb4753be71fd940590fc4edb6f49f5 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 26 Jul 2025 07:48:48 +0200 Subject: [PATCH 02/36] refactor(core): add util function to parse reporters from cli With --reporters flag the reporters can be given from the CLI --- .../parse-reporter-formats-from-cli.ts | 33 +++++++++++++++++ .../internal/supported-reporter-formats.ts | 1 + .../parse-reporter-formats-from-cli.spec.ts | 37 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 packages/core/src/lib/cli/internal/parse-reporter-formats-from-cli.ts create mode 100644 packages/core/src/lib/cli/internal/supported-reporter-formats.ts create mode 100644 packages/core/src/lib/cli/tests/parse-reporter-formats-from-cli.spec.ts diff --git a/packages/core/src/lib/cli/internal/parse-reporter-formats-from-cli.ts b/packages/core/src/lib/cli/internal/parse-reporter-formats-from-cli.ts new file mode 100644 index 00000000..c3cee563 --- /dev/null +++ b/packages/core/src/lib/cli/internal/parse-reporter-formats-from-cli.ts @@ -0,0 +1,33 @@ +import { cli } from '../cli'; +import { SUPPORTED_REPORTER_FORMATS } from './supported-reporter-formats'; + +export function parseReporterFormatsFromCli(args: string[]): string[] { + const reporters: string[] = []; + + for (const arg of args) { + if (arg.startsWith('--reporters=')) { + reporters.push( + ...arg + .slice('--reporters='.length) + .split(',') + .map((r) => r.trim()) + .filter((r) => r.length > 0), + ); + } + } + + validateReporterFormatsFromCli(reporters); + // filter out unsorted reporter formats + return reporters.filter((reporter) => + SUPPORTED_REPORTER_FORMATS.includes(reporter), + ); +} + +function validateReporterFormatsFromCli(reporters: string[]): void { + const validReporters = SUPPORTED_REPORTER_FORMATS; + for (const reporter of reporters) { + if (!validReporters.includes(reporter)) { + cli.logError(`Invalid reporter format: ${reporter}`); + } + } +} diff --git a/packages/core/src/lib/cli/internal/supported-reporter-formats.ts b/packages/core/src/lib/cli/internal/supported-reporter-formats.ts new file mode 100644 index 00000000..77c69747 --- /dev/null +++ b/packages/core/src/lib/cli/internal/supported-reporter-formats.ts @@ -0,0 +1 @@ +export const SUPPORTED_REPORTER_FORMATS = ['json', 'junit']; diff --git a/packages/core/src/lib/cli/tests/parse-reporter-formats-from-cli.spec.ts b/packages/core/src/lib/cli/tests/parse-reporter-formats-from-cli.spec.ts new file mode 100644 index 00000000..099da757 --- /dev/null +++ b/packages/core/src/lib/cli/tests/parse-reporter-formats-from-cli.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { parseReporterFormatsFromCli } from '../internal/parse-reporter-formats-from-cli'; + +describe('parseReporterFormatsFromCli', () => { + it('should extract correctly when a --reporters flag is given', () => { + expect( + parseReporterFormatsFromCli(['src/main.ts', '--reporters=junit']), + ).toEqual(['junit']); + }); + + it('should find --reporters flag at any position', () => { + expect( + parseReporterFormatsFromCli([ + 'src/main.ts', + 'A', + '--reporters=junit', + '--anotherOne=true', + ]), + ).toEqual(['junit']); + }); + + it('should return empty array when no --reporters flag is given', () => { + expect(parseReporterFormatsFromCli(['src/main.ts'])).toEqual([]); + }); + + it('should parse reporters which are comma separated', () => { + expect( + parseReporterFormatsFromCli(['src/main.ts', '--reporters=junit ,json']), + ).toEqual(['junit', 'json']); + }); + + it('should filter out not supported reporters', () => { + expect( + parseReporterFormatsFromCli(['src/main.ts', '--reporters=junit,a, b']), + ).toEqual(['junit']); + }); +}); From 1a0e31fa32c22243ae2863d1e94434051c984941 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 26 Jul 2025 07:58:58 +0200 Subject: [PATCH 03/36] refactor(core): add JSON-reporter --- .../cli/internal/reporter/junit-reporter.ts | 4 ++ .../cli/internal/reporter/reporter-factory.ts | 22 ++++++++ .../src/lib/cli/internal/reporter/reporter.ts | 5 ++ .../__snapshots__/json-reporter.spec.ts.snap | 29 ++++++++++ .../src/lib/cli/tests/json-reporter.spec.ts | 54 +++++++++++++++++++ 5 files changed, 114 insertions(+) create mode 100644 packages/core/src/lib/cli/internal/reporter/junit-reporter.ts create mode 100644 packages/core/src/lib/cli/internal/reporter/reporter-factory.ts create mode 100644 packages/core/src/lib/cli/internal/reporter/reporter.ts create mode 100644 packages/core/src/lib/cli/tests/__snapshots__/json-reporter.spec.ts.snap create mode 100644 packages/core/src/lib/cli/tests/json-reporter.spec.ts diff --git a/packages/core/src/lib/cli/internal/reporter/junit-reporter.ts b/packages/core/src/lib/cli/internal/reporter/junit-reporter.ts new file mode 100644 index 00000000..2ea90ecb --- /dev/null +++ b/packages/core/src/lib/cli/internal/reporter/junit-reporter.ts @@ -0,0 +1,4 @@ +import { Reporter } from './reporter'; + +// TODO +//export class JunitReporter implements Reporter {} diff --git a/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts b/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts new file mode 100644 index 00000000..bdc877fe --- /dev/null +++ b/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts @@ -0,0 +1,22 @@ +import { JsonReporter } from './json-reporter'; +import { Reporter } from './reporter'; + +export function reporterFactory(options: { + reporterFormats: string[]; + outputDir: string; + projectName: string; +}): Reporter[] { + const reporters: Reporter[] = []; + options.reporterFormats.forEach((format) => { + if (format === 'json') { + reporters.push( + new JsonReporter({ + outputDir: options.outputDir, + projectName: options.projectName, + }), + ); + } + }); + + return reporters; +} diff --git a/packages/core/src/lib/cli/internal/reporter/reporter.ts b/packages/core/src/lib/cli/internal/reporter/reporter.ts new file mode 100644 index 00000000..c18dec08 --- /dev/null +++ b/packages/core/src/lib/cli/internal/reporter/reporter.ts @@ -0,0 +1,5 @@ +import { SheriffViolations } from '../../sheriff-violations'; + +export interface Reporter { + createReport(validationResults: SheriffViolations): void; +} diff --git a/packages/core/src/lib/cli/tests/__snapshots__/json-reporter.spec.ts.snap b/packages/core/src/lib/cli/tests/__snapshots__/json-reporter.spec.ts.snap new file mode 100644 index 00000000..8336cb30 --- /dev/null +++ b/packages/core/src/lib/cli/tests/__snapshots__/json-reporter.spec.ts.snap @@ -0,0 +1,29 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`json reporter > should create a json-file in /.sheriff/project/violations.json 1`] = ` +"{ + "encapsulationsCount": 0, + "encapsulationValidations": [], + "dependencyRuleViolationsCount": 2, + "dependencyRuleViolations": [ + { + "rawImport": "@eternal/shared/master-data", + "fromModulePath": "/project/customers/feature", + "toModulePath": "/project/shared/master-data", + "fromTag": "domain:customers", + "toTags": [ + "shared:master-data" + ] + }, + { + "rawImport": "@eternal/shared/form", + "fromModulePath": "/project/customers/ui", + "toModulePath": "/project/app/shared/form", + "fromTag": "domain:customers", + "toTags": [ + "shared:form" + ] + } + ] +}" +`; diff --git a/packages/core/src/lib/cli/tests/json-reporter.spec.ts b/packages/core/src/lib/cli/tests/json-reporter.spec.ts new file mode 100644 index 00000000..b8f967ce --- /dev/null +++ b/packages/core/src/lib/cli/tests/json-reporter.spec.ts @@ -0,0 +1,54 @@ +import { beforeEach, describe, it, expect, beforeAll } from 'vitest'; +import { VirtualFs } from '../../fs/virtual-fs'; +import getFs, { useVirtualFs } from '../../fs/getFs'; +import { JsonReporter } from '../internal/reporter/json-reporter'; +import { toFsPath } from '../../file-info/fs-path'; + +import { SheriffViolations } from '../sheriff-violations'; + +describe('json reporter', () => { + let fs: VirtualFs; + beforeAll(() => { + useVirtualFs(); + fs = getFs() as VirtualFs; + }); + + beforeEach(() => { + fs.reset(); + fs.createDir('/project/customers/feature'); + fs.createDir('/project/shared/master-data'); + fs.createDir('/project/customers/ui'); + fs.createDir('/project/app/shared/form'); + }); + it('should create a json-file in /.sheriff/project/violations.json', () => { + const reporter = new JsonReporter(); + const violations: SheriffViolations = { + encapsulationsCount: 0, + encapsulationValidations: [], + dependencyRuleViolationsCount: 2, + dependencyRuleViolations: [ + { + rawImport: '@eternal/shared/master-data', + fromModulePath: toFsPath('/project/customers/feature'), + toModulePath: toFsPath('/project/shared/master-data'), + fromTag: 'domain:customers', + toTags: ['shared:master-data'], + }, + { + rawImport: '@eternal/shared/form', + fromModulePath: toFsPath('/project/customers/ui'), + toModulePath: toFsPath('/project/app/shared/form'), + fromTag: 'domain:customers', + toTags: ['shared:form'], + }, + ], + }; + reporter.createReport({ + exportDir: '.sheriff', + projectName: 'project', + validationResults: violations, + }); + + expect(fs.readFile('.sheriff/project/violations.json')).toMatchSnapshot(); + }); +}); From 98cab7ea24e1f43d2c90dad2349a9a95dd0348a0 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 26 Jul 2025 21:29:06 +0200 Subject: [PATCH 04/36] refactor(core): rename reportersDirectory to reportsDirectory --- packages/core/src/lib/config/default-config.ts | 2 +- packages/core/src/lib/config/user-sheriff-config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/lib/config/default-config.ts b/packages/core/src/lib/config/default-config.ts index 7f96b9ff..50975cb2 100644 --- a/packages/core/src/lib/config/default-config.ts +++ b/packages/core/src/lib/config/default-config.ts @@ -13,6 +13,6 @@ export const defaultConfig: Configuration = { isConfigFileMissing: false, barrelFileName: 'index.ts', entryPoints: undefined, - reportersDirectory: '.sheriff/reports', + reportsDirectory: '.sheriff/reports', defaultReporters: [], }; diff --git a/packages/core/src/lib/config/user-sheriff-config.ts b/packages/core/src/lib/config/user-sheriff-config.ts index 3ee92d8a..15895ee7 100644 --- a/packages/core/src/lib/config/user-sheriff-config.ts +++ b/packages/core/src/lib/config/user-sheriff-config.ts @@ -265,7 +265,7 @@ export interface UserSheriffConfig { * * Default is `./sheriff/reports` */ - reportersDirectory?: string; + reportsDirectory?: string; /** * The default reporters used to generate reports. */ From 976cf66f5b97fb7aa12ce51117bc02e0b216a9a1 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 26 Jul 2025 21:29:37 +0200 Subject: [PATCH 05/36] feat(core): add JSON Reporter --- .../cli/internal/reporter/json-reporter.ts | 30 +++++++++++++++++++ .../core/src/lib/cli/sheriff-violations.ts | 8 +++++ 2 files changed, 38 insertions(+) create mode 100644 packages/core/src/lib/cli/internal/reporter/json-reporter.ts create mode 100644 packages/core/src/lib/cli/sheriff-violations.ts diff --git a/packages/core/src/lib/cli/internal/reporter/json-reporter.ts b/packages/core/src/lib/cli/internal/reporter/json-reporter.ts new file mode 100644 index 00000000..8913a3a3 --- /dev/null +++ b/packages/core/src/lib/cli/internal/reporter/json-reporter.ts @@ -0,0 +1,30 @@ +import { Reporter } from './reporter'; +import { cli } from '../../cli'; +import getFs from '../../../fs/getFs'; + +import { SheriffViolations } from '../../sheriff-violations'; + +export class JsonReporter implements Reporter { + #options: { outputDir: string; projectName: string }; + constructor(options: { outputDir: string; projectName: string }) { + this.#options = options; + } + + createReport(validationResults: SheriffViolations): void { + const fs = getFs(); + const targetPath = fs.join( + this.#options.outputDir, + this.#options.projectName, + 'violations' + this.#getReportExtension(), + ); + cli.log(`Creating .json-export`); + + fs.createDir(fs.join(this.#options.outputDir, this.#options.projectName!)); + + fs.writeFile(targetPath, JSON.stringify(validationResults, null, 2)); + } + + #getReportExtension(): string { + return '.json'; + } +} diff --git a/packages/core/src/lib/cli/sheriff-violations.ts b/packages/core/src/lib/cli/sheriff-violations.ts new file mode 100644 index 00000000..3064559d --- /dev/null +++ b/packages/core/src/lib/cli/sheriff-violations.ts @@ -0,0 +1,8 @@ +import { DependencyRuleViolation } from '../checks/check-for-dependency-rule-violation'; + +export type SheriffViolations = { + encapsulationsCount: number; + encapsulationValidations: string[]; + dependencyRuleViolationsCount: number; + dependencyRuleViolations: DependencyRuleViolation[]; +}; From 7f7f21cb36ed0c1a5f0a8c45a7b4d303a92159d8 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 26 Jul 2025 21:39:24 +0200 Subject: [PATCH 06/36] feat(core): create reports --- .../core/src/lib/cli/sheriff-violations.ts | 10 ++-- packages/core/src/lib/cli/verify.ts | 58 ++++++++++++++++--- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/packages/core/src/lib/cli/sheriff-violations.ts b/packages/core/src/lib/cli/sheriff-violations.ts index 3064559d..ac496508 100644 --- a/packages/core/src/lib/cli/sheriff-violations.ts +++ b/packages/core/src/lib/cli/sheriff-violations.ts @@ -1,8 +1,10 @@ import { DependencyRuleViolation } from '../checks/check-for-dependency-rule-violation'; +import { ValidationsMap } from './verify'; export type SheriffViolations = { - encapsulationsCount: number; - encapsulationValidations: string[]; - dependencyRuleViolationsCount: number; - dependencyRuleViolations: DependencyRuleViolation[]; + deepImportsCount: number; + dependencyRulesCount: number; + filesCount: number; + hasError: boolean; + validationsMap: ValidationsMap; }; diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 59ce9fa1..2969a5b9 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -11,10 +11,18 @@ import { getEntriesFromCliOrConfig, } from './internal/get-entries-from-cli-or-config'; import { logInfoForMissingSheriffConfig } from './internal/log-info-for-missing-sheriff-config'; +import { parseReporterFormatsFromCli } from './internal/parse-reporter-formats-from-cli'; +import { reporterFactory } from './internal/reporter/reporter-factory'; +import { SheriffViolations } from './sheriff-violations'; -type ValidationsMap = Record< +export type ValidationsMap = Record< + // filepath relative to the project root string, - { encapsulations: string[]; dependencyRules: string[] } + { + encapsulations: string[]; + dependencyRules: string[]; + dependencyRuleViolations: DependencyRuleViolation[]; + } >; type ProjectValidation = { @@ -23,8 +31,6 @@ type ProjectValidation = { filesCount: number; hasError: boolean; validationsMap: ValidationsMap; - encapsulations: string[]; - dependencyRuleViolations: DependencyRuleViolation[]; }; export function verify(args: string[]) { @@ -48,8 +54,6 @@ export function verify(args: string[]) { filesCount: 0, hasError: false, validationsMap: {}, - encapsulations: [], - dependencyRuleViolations: [], }; projectValidations.set(projectName, validation); @@ -64,8 +68,6 @@ export function verify(args: string[]) { projectEntry.entry, ); const projectValidation = projectValidations.get(projectName)!; - projectValidation.encapsulations = encapsulations; - projectValidation.dependencyRuleViolations = dependencyRuleViolations; if (encapsulations.length > 0 || dependencyRuleViolations.length > 0) { projectValidation.hasError = true; @@ -84,6 +86,7 @@ export function verify(args: string[]) { projectValidation.validationsMap[relativePath] = { encapsulations, dependencyRules, + dependencyRuleViolations, }; } } @@ -139,6 +142,45 @@ export function verify(args: string[]) { } } + // --- + // TODO use projectEntries and the ValidationsMap to create now the reports let reporterFormats = []; + let reporterFormats = []; + for (const projectEntry of projectEntries) { + const projectName = projectEntry.projectName; + const projectValidation = projectValidations.get(projectName); + + // Read reporters from the CLI + reporterFormats = parseReporterFormatsFromCli(args); + + if (reporterFormats.length === 0) { + // if no reporters are given via the CLI we want to use the default reporters from the config + reporterFormats = projectEntry.entry.config.defaultReporters || []; + } + + if (reporterFormats.length > 0) { + const reportsDirectory = + projectEntry.entry.config.reportsDirectory || 'reports'; + const projectName = projectEntry.projectName; + const reporters = reporterFactory({ + reporterFormats: reporterFormats, + outputDir: reportsDirectory, + projectName, + }); + + if (projectValidation) { + const violations: SheriffViolations = { + ...projectValidation, + }; + + reporters.forEach((reporter) => { + reporter.createReport(violations); + }); + } + } + } + + // --- + // End process based on overall status if (hasAnyProjectError) { cli.endProcessError(); From f0d89ff22f9cd38bd3675fea34e750a7cad73cad Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 26 Jul 2025 22:00:39 +0200 Subject: [PATCH 07/36] refactor(core): simplify validationsmap --- .../core/src/lib/cli/sheriff-violations.ts | 2 +- packages/core/src/lib/cli/verify.ts | 34 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/core/src/lib/cli/sheriff-violations.ts b/packages/core/src/lib/cli/sheriff-violations.ts index ac496508..646f234d 100644 --- a/packages/core/src/lib/cli/sheriff-violations.ts +++ b/packages/core/src/lib/cli/sheriff-violations.ts @@ -6,5 +6,5 @@ export type SheriffViolations = { dependencyRulesCount: number; filesCount: number; hasError: boolean; - validationsMap: ValidationsMap; + validationsMap: ValidationsMap[]; }; diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 2969a5b9..5aabf1ab 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -15,22 +15,19 @@ import { parseReporterFormatsFromCli } from './internal/parse-reporter-formats-f import { reporterFactory } from './internal/reporter/reporter-factory'; import { SheriffViolations } from './sheriff-violations'; -export type ValidationsMap = Record< - // filepath relative to the project root - string, - { - encapsulations: string[]; - dependencyRules: string[]; - dependencyRuleViolations: DependencyRuleViolation[]; - } ->; +export type ValidationsMap = { + filePath: string; + encapsulations: string[]; + dependencyRules: string[]; + dependencyRuleViolations: DependencyRuleViolation[]; +}; type ProjectValidation = { deepImportsCount: number; dependencyRulesCount: number; filesCount: number; hasError: boolean; - validationsMap: ValidationsMap; + validationsMap: ValidationsMap[]; }; export function verify(args: string[]) { @@ -53,7 +50,7 @@ export function verify(args: string[]) { dependencyRulesCount: 0, filesCount: 0, hasError: false, - validationsMap: {}, + validationsMap: [], }; projectValidations.set(projectName, validation); @@ -83,11 +80,12 @@ export function verify(args: string[]) { ); const relativePath = fs.relativeTo(fs.cwd(), fileInfo.path); - projectValidation.validationsMap[relativePath] = { + projectValidation.validationsMap.push({ + filePath: relativePath, encapsulations, dependencyRules, dependencyRuleViolations, - }; + }); } } } @@ -116,10 +114,12 @@ export function verify(args: string[]) { cli.log(''); // Display detailed validation information for this project - for (const [file, { encapsulations, dependencyRules }] of Object.entries( - validation.validationsMap, - )) { - cli.log('|-- ' + file); + for (const { + encapsulations, + dependencyRules, + filePath, + } of validation.validationsMap) { + cli.log('|-- ' + filePath); if (encapsulations.length > 0) { cli.log('| |-- Encapsulation Violations'); encapsulations.forEach((encapsulation) => { From 8e0da2a076d6cd27271d79a1fea4ad3d9fb71270 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 26 Jul 2025 22:27:42 +0200 Subject: [PATCH 08/36] refactor(core): simplify creating reports --- packages/core/src/lib/cli/verify.ts | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 5aabf1ab..6bfeb96c 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -142,25 +142,24 @@ export function verify(args: string[]) { } } - // --- - // TODO use projectEntries and the ValidationsMap to create now the reports let reporterFormats = []; - let reporterFormats = []; - for (const projectEntry of projectEntries) { - const projectName = projectEntry.projectName; - const projectValidation = projectValidations.get(projectName); - - // Read reporters from the CLI - reporterFormats = parseReporterFormatsFromCli(args); - - if (reporterFormats.length === 0) { - // if no reporters are given via the CLI we want to use the default reporters from the config - reporterFormats = projectEntry.entry.config.defaultReporters || []; + // --- TODO outsource in function + // Read reporters from the CLI + let reporterFormats = parseReporterFormatsFromCli(args); + if (reporterFormats.length === 0) { + // if no reporters are given via the CLI we want to use the default reporters from the config + if (projectEntries.length > 0) { + reporterFormats = projectEntries[0].entry.config.defaultReporters || []; } + } + + if (reporterFormats.length > 0) { + for (const projectEntry of projectEntries) { + const projectName = projectEntry.projectName; + const projectValidation = projectValidations.get(projectName); - if (reporterFormats.length > 0) { const reportsDirectory = projectEntry.entry.config.reportsDirectory || 'reports'; - const projectName = projectEntry.projectName; + const reporters = reporterFactory({ reporterFormats: reporterFormats, outputDir: reportsDirectory, From 586bda3a099763e00d0f7d6c37fafc632388ab7a Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 26 Jul 2025 22:29:53 +0200 Subject: [PATCH 09/36] refactor(core): simplify creating reports --- packages/core/src/lib/cli/verify.ts | 31 ++++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 6bfeb96c..cd7a9976 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -14,6 +14,8 @@ import { logInfoForMissingSheriffConfig } from './internal/log-info-for-missing- import { parseReporterFormatsFromCli } from './internal/parse-reporter-formats-from-cli'; import { reporterFactory } from './internal/reporter/reporter-factory'; import { SheriffViolations } from './sheriff-violations'; +import { ProjectInfo } from '../main/init'; +import { Entry } from './internal/entry'; export type ValidationsMap = { filePath: string; @@ -142,7 +144,23 @@ export function verify(args: string[]) { } } - // --- TODO outsource in function + createReports(args, projectEntries, projectValidations); + + // End process based on overall status + if (hasAnyProjectError) { + cli.endProcessError(); + } else { + cli.log(''); + cli.log('\u001b[32mAll projects validated successfully!\u001b[0m'); + cli.endProcessOk(); + } +} + +function createReports( + args: string[], + projectEntries: Entry[], + projectValidations: Map, +) { // Read reporters from the CLI let reporterFormats = parseReporterFormatsFromCli(args); if (reporterFormats.length === 0) { @@ -177,15 +195,4 @@ export function verify(args: string[]) { } } } - - // --- - - // End process based on overall status - if (hasAnyProjectError) { - cli.endProcessError(); - } else { - cli.log(''); - cli.log('\u001b[32mAll projects validated successfully!\u001b[0m'); - cli.endProcessOk(); - } } From bf0be53b7343e9261c343f21a475096743268a20 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 09:32:03 +0200 Subject: [PATCH 10/36] refactor(core): rename --- packages/core/src/lib/cli/sheriff-violations.ts | 10 +++++----- packages/core/src/lib/cli/verify.ts | 11 ++++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/core/src/lib/cli/sheriff-violations.ts b/packages/core/src/lib/cli/sheriff-violations.ts index 646f234d..b24194e4 100644 --- a/packages/core/src/lib/cli/sheriff-violations.ts +++ b/packages/core/src/lib/cli/sheriff-violations.ts @@ -1,10 +1,10 @@ -import { DependencyRuleViolation } from '../checks/check-for-dependency-rule-violation'; import { ValidationsMap } from './verify'; export type SheriffViolations = { - deepImportsCount: number; - dependencyRulesCount: number; - filesCount: number; + totalDeepImportsViolations: number; + totalDependencyRulesViolations: number; + totalEncapsulationViolations: number; + totalViolatedFiles: number; hasError: boolean; - validationsMap: ValidationsMap[]; + violations: ValidationsMap[]; }; diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index cd7a9976..835e437f 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -27,6 +27,7 @@ export type ValidationsMap = { type ProjectValidation = { deepImportsCount: number; dependencyRulesCount: number; + encapsulationsCount: number; filesCount: number; hasError: boolean; validationsMap: ValidationsMap[]; @@ -50,6 +51,7 @@ export function verify(args: string[]) { const validation: ProjectValidation = { deepImportsCount: 0, dependencyRulesCount: 0, + encapsulationsCount: 0, filesCount: 0, hasError: false, validationsMap: [], @@ -75,6 +77,7 @@ export function verify(args: string[]) { projectValidation.dependencyRulesCount += dependencyRuleViolations.length; hasAnyProjectError = true; + projectValidation.encapsulationsCount += encapsulations.length; const dependencyRules = dependencyRuleViolations.map( (violation) => @@ -186,7 +189,13 @@ function createReports( if (projectValidation) { const violations: SheriffViolations = { - ...projectValidation, + hasError: projectValidation.hasError, + totalDeepImportsViolations: projectValidation.deepImportsCount, + totalDependencyRulesViolations: + projectValidation.dependencyRulesCount, + totalEncapsulationViolations: projectValidation.encapsulationsCount, + totalViolatedFiles: projectValidation.filesCount, + violations: projectValidation.validationsMap, }; reporters.forEach((reporter) => { From c36e9091220a332ac223438cd783c145a28b05af Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 09:54:05 +0200 Subject: [PATCH 11/36] fix(core): remove wrong assignment of deepImportsCount in verify Atm the violations of the deep imports is not part of verify --- packages/core/src/lib/cli/sheriff-violations.ts | 1 - packages/core/src/lib/cli/verify.ts | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/core/src/lib/cli/sheriff-violations.ts b/packages/core/src/lib/cli/sheriff-violations.ts index b24194e4..5d036a1e 100644 --- a/packages/core/src/lib/cli/sheriff-violations.ts +++ b/packages/core/src/lib/cli/sheriff-violations.ts @@ -1,7 +1,6 @@ import { ValidationsMap } from './verify'; export type SheriffViolations = { - totalDeepImportsViolations: number; totalDependencyRulesViolations: number; totalEncapsulationViolations: number; totalViolatedFiles: number; diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 835e437f..718ef811 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -25,7 +25,6 @@ export type ValidationsMap = { }; type ProjectValidation = { - deepImportsCount: number; dependencyRulesCount: number; encapsulationsCount: number; filesCount: number; @@ -49,7 +48,6 @@ export function verify(args: string[]) { // Initialize validation data for this project const validation: ProjectValidation = { - deepImportsCount: 0, dependencyRulesCount: 0, encapsulationsCount: 0, filesCount: 0, @@ -73,7 +71,6 @@ export function verify(args: string[]) { if (encapsulations.length > 0 || dependencyRuleViolations.length > 0) { projectValidation.hasError = true; projectValidation.filesCount++; - projectValidation.deepImportsCount += encapsulations.length; projectValidation.dependencyRulesCount += dependencyRuleViolations.length; hasAnyProjectError = true; @@ -110,7 +107,7 @@ export function verify(args: string[]) { cli.log('Issues found:'); cli.log(` Total Invalid Files: ${validation.filesCount}`); cli.log( - ` Total Encapsulation Violations: ${validation.deepImportsCount}`, + ` Total Encapsulation Violations: ${validation.encapsulationsCount}`, ); cli.log( ` Total Dependency Rule Violations: ${validation.dependencyRulesCount}`, @@ -190,7 +187,6 @@ function createReports( if (projectValidation) { const violations: SheriffViolations = { hasError: projectValidation.hasError, - totalDeepImportsViolations: projectValidation.deepImportsCount, totalDependencyRulesViolations: projectValidation.dependencyRulesCount, totalEncapsulationViolations: projectValidation.encapsulationsCount, From 2117e14fe0c0d93a60502985bbae157d8ad13932 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 10:16:40 +0200 Subject: [PATCH 12/36] feat(core): wip junit report builder --- .../junit/internal/junit-report-builder.ts | 119 ++++++++++++++++++ .../cli/tests/junit-report-builder.spec.ts | 61 +++++++++ 2 files changed, 180 insertions(+) create mode 100644 packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts create mode 100644 packages/core/src/lib/cli/tests/junit-report-builder.spec.ts diff --git a/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts b/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts new file mode 100644 index 00000000..b97339d6 --- /dev/null +++ b/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts @@ -0,0 +1,119 @@ +interface TestCase { + modulePath: string; + name: string; + failureMessage: string; + fromTag?: string; + toTags?: string; + fromModulePath?: string; + toModulePath?: string; +} + +interface TestSuite { + name: string; + testCases: TestCase[]; + addTestCase(testCase: TestCase): void; +} + +class JUnitTestSuite implements TestSuite { + name: string; + testCases: TestCase[] = []; + + constructor(name: string) { + this.name = name; + } + + addTestCase(testCase: TestCase): void { + this.testCases.push(testCase); + + // When adding an encapsulation violation, also add a deep-import violation for the same file + if (testCase.name === 'encapsulation') { + const deepImportCase: TestCase = { + modulePath: testCase.modulePath, + name: 'deep-import', + failureMessage: `.${testCase.modulePath} is a deep import from a barrel module. Use the module's barrel file (index.ts) instead.` + }; + this.testCases.push(deepImportCase); + } + } +} + +interface JUnitBuilder { + testsuite(name: string): TestSuite; + getReport(): string; +} + +class JUnitReportBuilder implements JUnitBuilder { + private testSuites: TestSuite[] = []; + + testsuite(name: string): TestSuite { + const suite = new JUnitTestSuite(name); + this.testSuites.push(suite); + return suite; + } + + getReport(): string { + if (this.testSuites.length === 0) { + return ` + +`; + } + + let report = ` +\n`; + + for (const suite of this.testSuites) { + const testSuite = suite as JUnitTestSuite; + + // Calculate violation counts + let totalDependencyRulesViolations = 0; + let totalEncapsulationViolations = 0; + const violatedFiles = new Set(); + + for (const testCase of testSuite.testCases) { + violatedFiles.add(testCase.modulePath); + if (testCase.name === 'dependency-rule') { + totalDependencyRulesViolations++; + } else if (testCase.name === 'encapsulation' || testCase.name === 'deep-import') { + totalEncapsulationViolations++; + } + } + + const hasError = testSuite.testCases.length > 0; + const totalViolatedFiles = violatedFiles.size; + + report += ` \n`; + + for (const testCase of testSuite.testCases) { + const attributes = [`modulePath="${testCase.modulePath}"`, `name="${testCase.name}"`]; + + if (testCase.fromTag) { + attributes.push(`fromTag="${testCase.fromTag}"`); + } + if (testCase.toTags) { + attributes.push(`toTags="${testCase.toTags}"`); + } + if (testCase.fromModulePath) { + attributes.push(`fromModulePath="${testCase.fromModulePath}"`); + } + if (testCase.toModulePath) { + attributes.push(`toModulePath="${testCase.toModulePath}"`); + } + + const hasSpaceBeforeClose = testCase.name === 'encapsulation' ? ' ' : ''; + report += ` \n`; + report += ` \n`; + report += ` \n`; + } + + report += ` \n`; + } + + report += ``; + + return report; + } +} + +export function junitBuilder(): JUnitBuilder { + return new JUnitReportBuilder(); +} \ No newline at end of file diff --git a/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts b/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts new file mode 100644 index 00000000..35064c90 --- /dev/null +++ b/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import { junitBuilder } from '../internal/reporter/junit/internal/junit-report-builder'; + +describe('JUnitReportBuilder', () => { + it('should create a new builder', () => { + const builder = junitBuilder(); + + expect(builder).toBeDefined(); + expect(builder.getReport()).toEqual(` + +`); + }); + it('testsuite() should create a new Testsuite ', () => { + const builder = junitBuilder(); + const suite = builder.testsuite('Example Suite'); + const expectedResult = ` + + + +`; + + expect(builder.getReport()).toEqual(expectedResult); + }); + + it('should add a new test case', () => { + const builder = junitBuilder(); + const suite = builder.testsuite('Example Suite'); + suite.addTestCase({ + modulePath: 'src/utils.ts', + name: 'encapsulation', + failureMessage: '.src/utils.ts cannot be imported. It is encapsulated.', + }); + suite.addTestCase({ + modulePath: 'src/app/shared/config/configuration.ts', + name: 'dependency-rule', + failureMessage: + 'module src/app/shared/config cannot access src/app/bookings. Tag shared has no clearance for tags domain:bookings,type:feature', + fromTag: 'shared', + toTags: 'domain:bookings,type:feature', + fromModulePath: 'src/app/shared/config/configuration.ts', + toModulePath: 'src/app/bookings', + }); + + const expectedResult = ` + + + + + + + + + + + + +`; + + expect(builder.getReport()).toEqual(expectedResult); + }); +}); From 60d8e3ef5a79d6a786388beaeca07090d8c4d604 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 10:30:43 +0200 Subject: [PATCH 13/36] feat(core): add JUnitReporter --- .../cli/internal/reporter/junit-reporter.ts | 4 -- .../internal/reporter/junit/junit-reporter.ts | 66 +++++++++++++++++++ .../cli/internal/reporter/reporter-factory.ts | 9 +++ 3 files changed, 75 insertions(+), 4 deletions(-) delete mode 100644 packages/core/src/lib/cli/internal/reporter/junit-reporter.ts create mode 100644 packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts diff --git a/packages/core/src/lib/cli/internal/reporter/junit-reporter.ts b/packages/core/src/lib/cli/internal/reporter/junit-reporter.ts deleted file mode 100644 index 2ea90ecb..00000000 --- a/packages/core/src/lib/cli/internal/reporter/junit-reporter.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Reporter } from './reporter'; - -// TODO -//export class JunitReporter implements Reporter {} diff --git a/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts b/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts new file mode 100644 index 00000000..d59654da --- /dev/null +++ b/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts @@ -0,0 +1,66 @@ +import { SheriffViolations } from '../../../sheriff-violations'; +import getFs from '../../../../fs/getFs'; +import { cli } from '../../../cli'; +import { Reporter } from '../reporter'; +import { junitBuilder } from './internal/junit-report-builder'; + +export class JunitReporter implements Reporter { + #options: { outputDir: string; projectName: string }; + constructor(options: { outputDir: string; projectName: string }) { + this.#options = options; + } + + createReport(validationResults: SheriffViolations) { + const fs = getFs(); + const targetPath = fs.join( + this.#options.outputDir, + this.#options.projectName, + 'violations' + this.#getReportExtension(), + ); + cli.log(`Creating JUnit XML report`); + + fs.createDir(fs.join(this.#options.outputDir, this.#options.projectName!)); + const xmlContent = this.#generateXml(validationResults); + fs.writeFile(targetPath, xmlContent); + } + + #getReportExtension(): string { + return '.xml'; + } + + #generateXml(validationResults: SheriffViolations): string { + const builder = junitBuilder(); + const suite = builder.testsuite(this.#options.projectName); + + // Process each violation + for (const violation of validationResults.violations) { + // Add encapsulation violations + for (const encapsulation of violation.encapsulations) { + suite.addTestCase({ + modulePath: violation.filePath, + name: 'encapsulation', + failureMessage: `${encapsulation} cannot be imported. It is encapsulated.`, + }); + } + + // Add dependency rule violations + for (const depViolation of violation.dependencyRuleViolations) { + const fromModulePath = depViolation.fromModulePath; + const toModulePath = depViolation.toModulePath; + + suite.addTestCase({ + modulePath: violation.filePath, + name: 'dependency-rule', + // TODO in verify we already build the message + failureMessage: `module ${fromModulePath} cannot access ${toModulePath}. Tag ${depViolation.fromTag} has no clearance for tags ${depViolation.toTags.join(',')}`, + fromTag: depViolation.fromTag, + toTags: depViolation.toTags.join(','), + fromModulePath: fromModulePath, + toModulePath: toModulePath, + }); + } + } + + return builder.getReport(); + } +} diff --git a/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts b/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts index bdc877fe..5ca816e0 100644 --- a/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts +++ b/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts @@ -1,5 +1,6 @@ import { JsonReporter } from './json-reporter'; import { Reporter } from './reporter'; +import { JunitReporter } from './junit/junit-reporter'; export function reporterFactory(options: { reporterFormats: string[]; @@ -16,6 +17,14 @@ export function reporterFactory(options: { }), ); } + if (format === 'junit') { + reporters.push( + new JunitReporter({ + outputDir: options.outputDir, + projectName: options.projectName, + }), + ); + } }); return reporters; From 784da741ba009fcc3e1ed0af2065504e173c6e92 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 10:31:52 +0200 Subject: [PATCH 14/36] refactor(core): move json reporter --- .../cli/internal/reporter/{ => json}/json-reporter.ts | 10 +++++----- .../src/lib/cli/internal/reporter/reporter-factory.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename packages/core/src/lib/cli/internal/reporter/{ => json}/json-reporter.ts (75%) diff --git a/packages/core/src/lib/cli/internal/reporter/json-reporter.ts b/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts similarity index 75% rename from packages/core/src/lib/cli/internal/reporter/json-reporter.ts rename to packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts index 8913a3a3..d6674beb 100644 --- a/packages/core/src/lib/cli/internal/reporter/json-reporter.ts +++ b/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts @@ -1,8 +1,8 @@ -import { Reporter } from './reporter'; -import { cli } from '../../cli'; -import getFs from '../../../fs/getFs'; +import { Reporter } from '../reporter'; +import { cli } from '../../../cli'; +import getFs from '../../../../fs/getFs'; -import { SheriffViolations } from '../../sheriff-violations'; +import { SheriffViolations } from '../../../sheriff-violations'; export class JsonReporter implements Reporter { #options: { outputDir: string; projectName: string }; @@ -17,7 +17,7 @@ export class JsonReporter implements Reporter { this.#options.projectName, 'violations' + this.#getReportExtension(), ); - cli.log(`Creating .json-export`); + cli.log(`Creating JSON-export`); fs.createDir(fs.join(this.#options.outputDir, this.#options.projectName!)); diff --git a/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts b/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts index 5ca816e0..cfded3ec 100644 --- a/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts +++ b/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts @@ -1,4 +1,4 @@ -import { JsonReporter } from './json-reporter'; +import { JsonReporter } from './json/json-reporter'; import { Reporter } from './reporter'; import { JunitReporter } from './junit/junit-reporter'; From 0398afab487b5861f44c506d4c99708956e283d5 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 10:35:27 +0200 Subject: [PATCH 15/36] refactor(core): add comment --- packages/core/src/lib/cli/verify.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 718ef811..c951b54a 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -164,7 +164,10 @@ function createReports( // Read reporters from the CLI let reporterFormats = parseReporterFormatsFromCli(args); if (reporterFormats.length === 0) { - // if no reporters are given via the CLI we want to use the default reporters from the config + /** + * if no reporters are given via the CLI we want to use the default reporters from the config. + * All Projects share the same config, so we can just take it from the first ProjectEntry. + */ if (projectEntries.length > 0) { reporterFormats = projectEntries[0].entry.config.defaultReporters || []; } From 40dc7bce3ba876621608cb08460f4ae932dd289c Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 10:40:11 +0200 Subject: [PATCH 16/36] fix(core): do not create /default directoy for single workspace projects --- .../internal/reporter/json/json-reporter.ts | 26 ++++++++++++++----- .../internal/reporter/junit/junit-reporter.ts | 25 +++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts b/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts index d6674beb..164615b3 100644 --- a/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts +++ b/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts @@ -3,6 +3,7 @@ import { cli } from '../../../cli'; import getFs from '../../../../fs/getFs'; import { SheriffViolations } from '../../../sheriff-violations'; +import { DEFAULT_PROJECT_NAME } from '../../get-entries-from-cli-or-config'; export class JsonReporter implements Reporter { #options: { outputDir: string; projectName: string }; @@ -12,14 +13,25 @@ export class JsonReporter implements Reporter { createReport(validationResults: SheriffViolations): void { const fs = getFs(); - const targetPath = fs.join( - this.#options.outputDir, - this.#options.projectName, - 'violations' + this.#getReportExtension(), - ); + const targetPath = + this.#options.projectName === DEFAULT_PROJECT_NAME + ? fs.join( + this.#options.outputDir, + 'violations' + this.#getReportExtension(), + ) + : fs.join( + this.#options.outputDir, + this.#options.projectName, + 'violations' + this.#getReportExtension(), + ); cli.log(`Creating JSON-export`); - - fs.createDir(fs.join(this.#options.outputDir, this.#options.projectName!)); + if (this.#options.projectName === DEFAULT_PROJECT_NAME) { + fs.createDir(fs.join(this.#options.outputDir)); + } else { + fs.createDir( + fs.join(this.#options.outputDir, this.#options.projectName!), + ); + } fs.writeFile(targetPath, JSON.stringify(validationResults, null, 2)); } diff --git a/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts b/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts index d59654da..426278ff 100644 --- a/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts +++ b/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts @@ -3,6 +3,7 @@ import getFs from '../../../../fs/getFs'; import { cli } from '../../../cli'; import { Reporter } from '../reporter'; import { junitBuilder } from './internal/junit-report-builder'; +import { DEFAULT_PROJECT_NAME } from '../../get-entries-from-cli-or-config'; export class JunitReporter implements Reporter { #options: { outputDir: string; projectName: string }; @@ -12,14 +13,26 @@ export class JunitReporter implements Reporter { createReport(validationResults: SheriffViolations) { const fs = getFs(); - const targetPath = fs.join( - this.#options.outputDir, - this.#options.projectName, - 'violations' + this.#getReportExtension(), - ); + const targetPath = + this.#options.projectName === DEFAULT_PROJECT_NAME + ? fs.join( + this.#options.outputDir, + 'violations' + this.#getReportExtension(), + ) + : fs.join( + this.#options.outputDir, + this.#options.projectName, + 'violations' + this.#getReportExtension(), + ); cli.log(`Creating JUnit XML report`); - fs.createDir(fs.join(this.#options.outputDir, this.#options.projectName!)); + if (this.#options.projectName === DEFAULT_PROJECT_NAME) { + fs.createDir(fs.join(this.#options.outputDir)); + } else { + fs.createDir( + fs.join(this.#options.outputDir, this.#options.projectName!), + ); + } const xmlContent = this.#generateXml(validationResults); fs.writeFile(targetPath, xmlContent); } From 48750f2fb8c89077defbb79de5d00d9dc84edb7f Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 15:11:06 +0200 Subject: [PATCH 17/36] refactor(core): rename --- .../junit/internal/junit-report-builder.ts | 55 ++++++++++--------- .../internal/reporter/junit/junit-reporter.ts | 10 +++- .../core/src/lib/cli/sheriff-violations.ts | 2 +- .../cli/tests/junit-report-builder.spec.ts | 18 +++++- packages/core/src/lib/cli/verify.ts | 3 +- 5 files changed, 55 insertions(+), 33 deletions(-) diff --git a/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts b/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts index b97339d6..506cc5cf 100644 --- a/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts +++ b/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts @@ -8,23 +8,43 @@ interface TestCase { toModulePath?: string; } +interface TestSuiteOptions { + name: string; + totalDependencyRulesViolations: number; + totalEncapsulationViolations: number; + totalViolatedFiles: number; + hasError: boolean; +} + interface TestSuite { name: string; + totalDependencyRulesViolations: number; + totalEncapsulationViolations: number; + totalViolatedFiles: number; + hasError: boolean; testCases: TestCase[]; addTestCase(testCase: TestCase): void; } class JUnitTestSuite implements TestSuite { name: string; + totalDependencyRulesViolations: number; + totalEncapsulationViolations: number; + totalViolatedFiles: number; + hasError: boolean; testCases: TestCase[] = []; - constructor(name: string) { - this.name = name; + constructor(options: TestSuiteOptions) { + this.name = options.name; + this.totalDependencyRulesViolations = options.totalDependencyRulesViolations; + this.totalEncapsulationViolations = options.totalEncapsulationViolations; + this.totalViolatedFiles = options.totalViolatedFiles; + this.hasError = options.hasError; } addTestCase(testCase: TestCase): void { this.testCases.push(testCase); - + // When adding an encapsulation violation, also add a deep-import violation for the same file if (testCase.name === 'encapsulation') { const deepImportCase: TestCase = { @@ -38,15 +58,15 @@ class JUnitTestSuite implements TestSuite { } interface JUnitBuilder { - testsuite(name: string): TestSuite; + testsuite(options: TestSuiteOptions): TestSuite; getReport(): string; } class JUnitReportBuilder implements JUnitBuilder { private testSuites: TestSuite[] = []; - testsuite(name: string): TestSuite { - const suite = new JUnitTestSuite(name); + testsuite(options: TestSuiteOptions): TestSuite { + const suite = new JUnitTestSuite(options); this.testSuites.push(suite); return suite; } @@ -63,29 +83,12 @@ class JUnitReportBuilder implements JUnitBuilder { for (const suite of this.testSuites) { const testSuite = suite as JUnitTestSuite; - - // Calculate violation counts - let totalDependencyRulesViolations = 0; - let totalEncapsulationViolations = 0; - const violatedFiles = new Set(); - for (const testCase of testSuite.testCases) { - violatedFiles.add(testCase.modulePath); - if (testCase.name === 'dependency-rule') { - totalDependencyRulesViolations++; - } else if (testCase.name === 'encapsulation' || testCase.name === 'deep-import') { - totalEncapsulationViolations++; - } - } - - const hasError = testSuite.testCases.length > 0; - const totalViolatedFiles = violatedFiles.size; - - report += ` \n`; + report += ` \n`; for (const testCase of testSuite.testCases) { const attributes = [`modulePath="${testCase.modulePath}"`, `name="${testCase.name}"`]; - + if (testCase.fromTag) { attributes.push(`fromTag="${testCase.fromTag}"`); } @@ -116,4 +119,4 @@ class JUnitReportBuilder implements JUnitBuilder { export function junitBuilder(): JUnitBuilder { return new JUnitReportBuilder(); -} \ No newline at end of file +} diff --git a/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts b/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts index 426278ff..d74d29e5 100644 --- a/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts +++ b/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts @@ -43,7 +43,15 @@ export class JunitReporter implements Reporter { #generateXml(validationResults: SheriffViolations): string { const builder = junitBuilder(); - const suite = builder.testsuite(this.#options.projectName); + const suite = builder.testsuite({ + name: this.#options.projectName, + totalDependencyRulesViolations: + validationResults.totalDependencyRuleViolations, + totalEncapsulationViolations: + validationResults.totalEncapsulationViolations, + totalViolatedFiles: validationResults.totalViolatedFiles, + hasError: validationResults.hasError, + }); // Process each violation for (const violation of validationResults.violations) { diff --git a/packages/core/src/lib/cli/sheriff-violations.ts b/packages/core/src/lib/cli/sheriff-violations.ts index 5d036a1e..ab02df09 100644 --- a/packages/core/src/lib/cli/sheriff-violations.ts +++ b/packages/core/src/lib/cli/sheriff-violations.ts @@ -1,7 +1,7 @@ import { ValidationsMap } from './verify'; export type SheriffViolations = { - totalDependencyRulesViolations: number; + totalDependencyRuleViolations: number; totalEncapsulationViolations: number; totalViolatedFiles: number; hasError: boolean; diff --git a/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts b/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts index 35064c90..b0646757 100644 --- a/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts +++ b/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts @@ -12,10 +12,16 @@ describe('JUnitReportBuilder', () => { }); it('testsuite() should create a new Testsuite ', () => { const builder = junitBuilder(); - const suite = builder.testsuite('Example Suite'); + const suite = builder.testsuite({ + name: 'Example Suite', + totalDependencyRulesViolations: 1, + totalEncapsulationViolations: 2, + totalViolatedFiles: 2, + hasError: true, + }); const expectedResult = ` - + `; @@ -24,7 +30,13 @@ describe('JUnitReportBuilder', () => { it('should add a new test case', () => { const builder = junitBuilder(); - const suite = builder.testsuite('Example Suite'); + const suite = builder.testsuite({ + name: 'Example Suite', + totalDependencyRulesViolations: 1, + totalEncapsulationViolations: 2, + totalViolatedFiles: 2, + hasError: true, + }); suite.addTestCase({ modulePath: 'src/utils.ts', name: 'encapsulation', diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index c951b54a..8c3b3961 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -190,8 +190,7 @@ function createReports( if (projectValidation) { const violations: SheriffViolations = { hasError: projectValidation.hasError, - totalDependencyRulesViolations: - projectValidation.dependencyRulesCount, + totalDependencyRuleViolations: projectValidation.dependencyRulesCount, totalEncapsulationViolations: projectValidation.encapsulationsCount, totalViolatedFiles: projectValidation.filesCount, violations: projectValidation.validationsMap, From baa853fbfc6848230550670aed17961f41a432e2 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 19:14:33 +0200 Subject: [PATCH 18/36] refactor(core): move files --- .../internal/{ => reporter}/parse-reporter-formats-from-cli.ts | 2 +- .../cli/internal/{ => reporter}/supported-reporter-formats.ts | 0 packages/core/src/lib/cli/verify.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/core/src/lib/cli/internal/{ => reporter}/parse-reporter-formats-from-cli.ts (96%) rename packages/core/src/lib/cli/internal/{ => reporter}/supported-reporter-formats.ts (100%) diff --git a/packages/core/src/lib/cli/internal/parse-reporter-formats-from-cli.ts b/packages/core/src/lib/cli/internal/reporter/parse-reporter-formats-from-cli.ts similarity index 96% rename from packages/core/src/lib/cli/internal/parse-reporter-formats-from-cli.ts rename to packages/core/src/lib/cli/internal/reporter/parse-reporter-formats-from-cli.ts index c3cee563..68e48daa 100644 --- a/packages/core/src/lib/cli/internal/parse-reporter-formats-from-cli.ts +++ b/packages/core/src/lib/cli/internal/reporter/parse-reporter-formats-from-cli.ts @@ -1,4 +1,4 @@ -import { cli } from '../cli'; +import { cli } from '../../cli'; import { SUPPORTED_REPORTER_FORMATS } from './supported-reporter-formats'; export function parseReporterFormatsFromCli(args: string[]): string[] { diff --git a/packages/core/src/lib/cli/internal/supported-reporter-formats.ts b/packages/core/src/lib/cli/internal/reporter/supported-reporter-formats.ts similarity index 100% rename from packages/core/src/lib/cli/internal/supported-reporter-formats.ts rename to packages/core/src/lib/cli/internal/reporter/supported-reporter-formats.ts diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 8c3b3961..9636d36c 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -11,7 +11,7 @@ import { getEntriesFromCliOrConfig, } from './internal/get-entries-from-cli-or-config'; import { logInfoForMissingSheriffConfig } from './internal/log-info-for-missing-sheriff-config'; -import { parseReporterFormatsFromCli } from './internal/parse-reporter-formats-from-cli'; +import { parseReporterFormatsFromCli } from './internal/reporter/parse-reporter-formats-from-cli'; import { reporterFactory } from './internal/reporter/reporter-factory'; import { SheriffViolations } from './sheriff-violations'; import { ProjectInfo } from '../main/init'; From 7db35c7e1839db4345970bfbea2fcef7e4507201 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 20:08:25 +0200 Subject: [PATCH 19/36] fix(core): remove cli flags starting with -- from args --- .../core/src/lib/cli/internal/remove-cli-flags-from-args.ts | 3 +++ packages/core/src/lib/cli/verify.ts | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/lib/cli/internal/remove-cli-flags-from-args.ts diff --git a/packages/core/src/lib/cli/internal/remove-cli-flags-from-args.ts b/packages/core/src/lib/cli/internal/remove-cli-flags-from-args.ts new file mode 100644 index 00000000..075e363d --- /dev/null +++ b/packages/core/src/lib/cli/internal/remove-cli-flags-from-args.ts @@ -0,0 +1,3 @@ +export function removeCliFlagsFromArgs(args: string[]): string[] { + return args.filter((arg) => !arg.startsWith('--')); +} diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 9636d36c..03a1c6d2 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -16,6 +16,7 @@ import { reporterFactory } from './internal/reporter/reporter-factory'; import { SheriffViolations } from './sheriff-violations'; import { ProjectInfo } from '../main/init'; import { Entry } from './internal/entry'; +import { removeCliFlagsFromArgs } from './internal/remove-cli-flags-from-args'; export type ValidationsMap = { filePath: string; @@ -33,8 +34,11 @@ type ProjectValidation = { }; export function verify(args: string[]) { + console.log('verify ', args); const fs = getFs(); - const projectEntries = getEntriesFromCliOrConfig(args[0]); + const projectEntries = getEntriesFromCliOrConfig( + removeCliFlagsFromArgs(args)[0], + ); logInfoForMissingSheriffConfig(projectEntries[0].entry); // Keep track of overall status to determine final process exit code From 9c27193b20343ae1a2f2c9c3792a24aac61c4c65 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 21:20:55 +0200 Subject: [PATCH 20/36] refactor(core): drop possibility to provide reporters via cli --- .../internal/remove-cli-flags-from-args.ts | 3 -- .../parse-reporter-formats-from-cli.ts | 33 ----------------- .../parse-reporter-formats-from-cli.spec.ts | 37 ------------------- packages/core/src/lib/cli/verify.ts | 17 +-------- 4 files changed, 2 insertions(+), 88 deletions(-) delete mode 100644 packages/core/src/lib/cli/internal/remove-cli-flags-from-args.ts delete mode 100644 packages/core/src/lib/cli/internal/reporter/parse-reporter-formats-from-cli.ts delete mode 100644 packages/core/src/lib/cli/tests/parse-reporter-formats-from-cli.spec.ts diff --git a/packages/core/src/lib/cli/internal/remove-cli-flags-from-args.ts b/packages/core/src/lib/cli/internal/remove-cli-flags-from-args.ts deleted file mode 100644 index 075e363d..00000000 --- a/packages/core/src/lib/cli/internal/remove-cli-flags-from-args.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function removeCliFlagsFromArgs(args: string[]): string[] { - return args.filter((arg) => !arg.startsWith('--')); -} diff --git a/packages/core/src/lib/cli/internal/reporter/parse-reporter-formats-from-cli.ts b/packages/core/src/lib/cli/internal/reporter/parse-reporter-formats-from-cli.ts deleted file mode 100644 index 68e48daa..00000000 --- a/packages/core/src/lib/cli/internal/reporter/parse-reporter-formats-from-cli.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { cli } from '../../cli'; -import { SUPPORTED_REPORTER_FORMATS } from './supported-reporter-formats'; - -export function parseReporterFormatsFromCli(args: string[]): string[] { - const reporters: string[] = []; - - for (const arg of args) { - if (arg.startsWith('--reporters=')) { - reporters.push( - ...arg - .slice('--reporters='.length) - .split(',') - .map((r) => r.trim()) - .filter((r) => r.length > 0), - ); - } - } - - validateReporterFormatsFromCli(reporters); - // filter out unsorted reporter formats - return reporters.filter((reporter) => - SUPPORTED_REPORTER_FORMATS.includes(reporter), - ); -} - -function validateReporterFormatsFromCli(reporters: string[]): void { - const validReporters = SUPPORTED_REPORTER_FORMATS; - for (const reporter of reporters) { - if (!validReporters.includes(reporter)) { - cli.logError(`Invalid reporter format: ${reporter}`); - } - } -} diff --git a/packages/core/src/lib/cli/tests/parse-reporter-formats-from-cli.spec.ts b/packages/core/src/lib/cli/tests/parse-reporter-formats-from-cli.spec.ts deleted file mode 100644 index 099da757..00000000 --- a/packages/core/src/lib/cli/tests/parse-reporter-formats-from-cli.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { parseReporterFormatsFromCli } from '../internal/parse-reporter-formats-from-cli'; - -describe('parseReporterFormatsFromCli', () => { - it('should extract correctly when a --reporters flag is given', () => { - expect( - parseReporterFormatsFromCli(['src/main.ts', '--reporters=junit']), - ).toEqual(['junit']); - }); - - it('should find --reporters flag at any position', () => { - expect( - parseReporterFormatsFromCli([ - 'src/main.ts', - 'A', - '--reporters=junit', - '--anotherOne=true', - ]), - ).toEqual(['junit']); - }); - - it('should return empty array when no --reporters flag is given', () => { - expect(parseReporterFormatsFromCli(['src/main.ts'])).toEqual([]); - }); - - it('should parse reporters which are comma separated', () => { - expect( - parseReporterFormatsFromCli(['src/main.ts', '--reporters=junit ,json']), - ).toEqual(['junit', 'json']); - }); - - it('should filter out not supported reporters', () => { - expect( - parseReporterFormatsFromCli(['src/main.ts', '--reporters=junit,a, b']), - ).toEqual(['junit']); - }); -}); diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 03a1c6d2..12482912 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -11,12 +11,10 @@ import { getEntriesFromCliOrConfig, } from './internal/get-entries-from-cli-or-config'; import { logInfoForMissingSheriffConfig } from './internal/log-info-for-missing-sheriff-config'; -import { parseReporterFormatsFromCli } from './internal/reporter/parse-reporter-formats-from-cli'; import { reporterFactory } from './internal/reporter/reporter-factory'; import { SheriffViolations } from './sheriff-violations'; import { ProjectInfo } from '../main/init'; import { Entry } from './internal/entry'; -import { removeCliFlagsFromArgs } from './internal/remove-cli-flags-from-args'; export type ValidationsMap = { filePath: string; @@ -36,9 +34,7 @@ type ProjectValidation = { export function verify(args: string[]) { console.log('verify ', args); const fs = getFs(); - const projectEntries = getEntriesFromCliOrConfig( - removeCliFlagsFromArgs(args)[0], - ); + const projectEntries = getEntriesFromCliOrConfig(args[0]); logInfoForMissingSheriffConfig(projectEntries[0].entry); // Keep track of overall status to determine final process exit code @@ -166,16 +162,7 @@ function createReports( projectValidations: Map, ) { // Read reporters from the CLI - let reporterFormats = parseReporterFormatsFromCli(args); - if (reporterFormats.length === 0) { - /** - * if no reporters are given via the CLI we want to use the default reporters from the config. - * All Projects share the same config, so we can just take it from the first ProjectEntry. - */ - if (projectEntries.length > 0) { - reporterFormats = projectEntries[0].entry.config.defaultReporters || []; - } - } + const reporterFormats = projectEntries[0].entry.config.defaultReporters || []; if (reporterFormats.length > 0) { for (const projectEntry of projectEntries) { From 9057586e559addc8ef0ed1aa022a9747c285eb64 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 21:21:44 +0200 Subject: [PATCH 21/36] refactor(core): rename defaultReporter to reporters in config --- packages/core/src/lib/cli/verify.ts | 2 +- packages/core/src/lib/config/default-config.ts | 2 +- packages/core/src/lib/config/user-sheriff-config.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 12482912..92795fa1 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -162,7 +162,7 @@ function createReports( projectValidations: Map, ) { // Read reporters from the CLI - const reporterFormats = projectEntries[0].entry.config.defaultReporters || []; + const reporterFormats = projectEntries[0].entry.config.reporters || []; if (reporterFormats.length > 0) { for (const projectEntry of projectEntries) { diff --git a/packages/core/src/lib/config/default-config.ts b/packages/core/src/lib/config/default-config.ts index 50975cb2..eaab6ace 100644 --- a/packages/core/src/lib/config/default-config.ts +++ b/packages/core/src/lib/config/default-config.ts @@ -14,5 +14,5 @@ export const defaultConfig: Configuration = { barrelFileName: 'index.ts', entryPoints: undefined, reportsDirectory: '.sheriff/reports', - defaultReporters: [], + reporters: [], }; diff --git a/packages/core/src/lib/config/user-sheriff-config.ts b/packages/core/src/lib/config/user-sheriff-config.ts index 15895ee7..85bd0b58 100644 --- a/packages/core/src/lib/config/user-sheriff-config.ts +++ b/packages/core/src/lib/config/user-sheriff-config.ts @@ -267,7 +267,7 @@ export interface UserSheriffConfig { */ reportsDirectory?: string; /** - * The default reporters used to generate reports. + * The reporters used to generate reports. */ - defaultReporters?: string[]; + reporters?: string[]; } From 3208561d2df5730d126f1200ea3db24ad59e40e4 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 21:29:41 +0200 Subject: [PATCH 22/36] chore(core): update tests --- .../core/src/lib/cli/sheriff-violations.ts | 4 +- .../__snapshots__/json-reporter.spec.ts.snap | 47 +++++++++------ .../src/lib/cli/tests/json-reporter.spec.ts | 57 ++++++++++++------- packages/core/src/lib/cli/verify.ts | 4 +- .../src/lib/config/tests/parse-config.spec.ts | 4 ++ 5 files changed, 74 insertions(+), 42 deletions(-) diff --git a/packages/core/src/lib/cli/sheriff-violations.ts b/packages/core/src/lib/cli/sheriff-violations.ts index ab02df09..2772ee76 100644 --- a/packages/core/src/lib/cli/sheriff-violations.ts +++ b/packages/core/src/lib/cli/sheriff-violations.ts @@ -1,9 +1,9 @@ -import { ValidationsMap } from './verify'; +import { Violation } from './verify'; export type SheriffViolations = { totalDependencyRuleViolations: number; totalEncapsulationViolations: number; totalViolatedFiles: number; hasError: boolean; - violations: ValidationsMap[]; + violations: Violation[]; }; diff --git a/packages/core/src/lib/cli/tests/__snapshots__/json-reporter.spec.ts.snap b/packages/core/src/lib/cli/tests/__snapshots__/json-reporter.spec.ts.snap index 8336cb30..d23c3d11 100644 --- a/packages/core/src/lib/cli/tests/__snapshots__/json-reporter.spec.ts.snap +++ b/packages/core/src/lib/cli/tests/__snapshots__/json-reporter.spec.ts.snap @@ -2,26 +2,41 @@ exports[`json reporter > should create a json-file in /.sheriff/project/violations.json 1`] = ` "{ - "encapsulationsCount": 0, - "encapsulationValidations": [], - "dependencyRuleViolationsCount": 2, - "dependencyRuleViolations": [ + "totalEncapsulationViolations": 0, + "totalViolatedFiles": 0, + "totalDependencyRuleViolations": 2, + "hasError": true, + "violations": [ { - "rawImport": "@eternal/shared/master-data", - "fromModulePath": "/project/customers/feature", - "toModulePath": "/project/shared/master-data", - "fromTag": "domain:customers", - "toTags": [ - "shared:master-data" + "filePath": "/project/customers/feature/feature.ts", + "encapsulations": [], + "dependencyRules": [], + "dependencyRuleViolations": [ + { + "rawImport": "@eternal/shared/master-data", + "fromModulePath": "/project/customers/feature", + "toModulePath": "/project/shared/master-data", + "fromTag": "domain:customers", + "toTags": [ + "shared:master-data" + ] + } ] }, { - "rawImport": "@eternal/shared/form", - "fromModulePath": "/project/customers/ui", - "toModulePath": "/project/app/shared/form", - "fromTag": "domain:customers", - "toTags": [ - "shared:form" + "filePath": "/project/customers/ui/ui.ts", + "encapsulations": [], + "dependencyRules": [], + "dependencyRuleViolations": [ + { + "rawImport": "@eternal/shared/form", + "fromModulePath": "/project/customers/ui", + "toModulePath": "/project/app/shared/form", + "fromTag": "domain:customers", + "toTags": [ + "shared:form" + ] + } ] } ] diff --git a/packages/core/src/lib/cli/tests/json-reporter.spec.ts b/packages/core/src/lib/cli/tests/json-reporter.spec.ts index b8f967ce..b7a7c727 100644 --- a/packages/core/src/lib/cli/tests/json-reporter.spec.ts +++ b/packages/core/src/lib/cli/tests/json-reporter.spec.ts @@ -1,10 +1,9 @@ import { beforeEach, describe, it, expect, beforeAll } from 'vitest'; import { VirtualFs } from '../../fs/virtual-fs'; import getFs, { useVirtualFs } from '../../fs/getFs'; -import { JsonReporter } from '../internal/reporter/json-reporter'; import { toFsPath } from '../../file-info/fs-path'; - import { SheriffViolations } from '../sheriff-violations'; +import { JsonReporter } from '../internal/reporter/json/json-reporter'; describe('json reporter', () => { let fs: VirtualFs; @@ -21,33 +20,47 @@ describe('json reporter', () => { fs.createDir('/project/app/shared/form'); }); it('should create a json-file in /.sheriff/project/violations.json', () => { - const reporter = new JsonReporter(); + const reporter = new JsonReporter({ + outputDir: '.sheriff', + projectName: 'project', + }); const violations: SheriffViolations = { - encapsulationsCount: 0, - encapsulationValidations: [], - dependencyRuleViolationsCount: 2, - dependencyRuleViolations: [ + totalEncapsulationViolations: 0, + totalViolatedFiles: 0, + totalDependencyRuleViolations: 2, + hasError: true, + violations: [ { - rawImport: '@eternal/shared/master-data', - fromModulePath: toFsPath('/project/customers/feature'), - toModulePath: toFsPath('/project/shared/master-data'), - fromTag: 'domain:customers', - toTags: ['shared:master-data'], + filePath: '/project/customers/feature/feature.ts', + encapsulations: [], + dependencyRules: [], + dependencyRuleViolations: [ + { + rawImport: '@eternal/shared/master-data', + fromModulePath: toFsPath('/project/customers/feature'), + toModulePath: toFsPath('/project/shared/master-data'), + fromTag: 'domain:customers', + toTags: ['shared:master-data'], + }, + ], }, { - rawImport: '@eternal/shared/form', - fromModulePath: toFsPath('/project/customers/ui'), - toModulePath: toFsPath('/project/app/shared/form'), - fromTag: 'domain:customers', - toTags: ['shared:form'], + filePath: '/project/customers/ui/ui.ts', + encapsulations: [], + dependencyRules: [], + dependencyRuleViolations: [ + { + rawImport: '@eternal/shared/form', + fromModulePath: toFsPath('/project/customers/ui'), + toModulePath: toFsPath('/project/app/shared/form'), + fromTag: 'domain:customers', + toTags: ['shared:form'], + }, + ], }, ], }; - reporter.createReport({ - exportDir: '.sheriff', - projectName: 'project', - validationResults: violations, - }); + reporter.createReport(violations); expect(fs.readFile('.sheriff/project/violations.json')).toMatchSnapshot(); }); diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index 92795fa1..eabbb922 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -16,7 +16,7 @@ import { SheriffViolations } from './sheriff-violations'; import { ProjectInfo } from '../main/init'; import { Entry } from './internal/entry'; -export type ValidationsMap = { +export type Violation = { filePath: string; encapsulations: string[]; dependencyRules: string[]; @@ -28,7 +28,7 @@ type ProjectValidation = { encapsulationsCount: number; filesCount: number; hasError: boolean; - validationsMap: ValidationsMap[]; + validationsMap: Violation[]; }; export function verify(args: string[]) { diff --git a/packages/core/src/lib/config/tests/parse-config.spec.ts b/packages/core/src/lib/config/tests/parse-config.spec.ts index 6c11f740..c59047e4 100644 --- a/packages/core/src/lib/config/tests/parse-config.spec.ts +++ b/packages/core/src/lib/config/tests/parse-config.spec.ts @@ -40,6 +40,8 @@ describe('parse Config', () => { 'isConfigFileMissing', 'barrelFileName', 'entryPoints', + 'reportsDirectory', + 'reporters', ]); }); @@ -81,6 +83,8 @@ export const config: SheriffConfig = { isConfigFileMissing: false, entryFile: '', barrelFileName: 'index.ts', + reporters: [], + reportsDirectory: '.sheriff/reports', }); }); From 8c0914d8178ef523ed32d51367ccc532f9fa45e6 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 21:32:44 +0200 Subject: [PATCH 23/36] chore(core): add tests --- .../__snapshots__/junit-reporter.spec.ts.snap | 15 +++++ .../src/lib/cli/tests/junit-reporter.spec.ts | 67 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 packages/core/src/lib/cli/tests/__snapshots__/junit-reporter.spec.ts.snap create mode 100644 packages/core/src/lib/cli/tests/junit-reporter.spec.ts diff --git a/packages/core/src/lib/cli/tests/__snapshots__/junit-reporter.spec.ts.snap b/packages/core/src/lib/cli/tests/__snapshots__/junit-reporter.spec.ts.snap new file mode 100644 index 00000000..3121eb7d --- /dev/null +++ b/packages/core/src/lib/cli/tests/__snapshots__/junit-reporter.spec.ts.snap @@ -0,0 +1,15 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`JUnit reporter > should create a xml-file in /.sheriff/project/violations.xml 1`] = ` +" + + + + + + + + + +" +`; diff --git a/packages/core/src/lib/cli/tests/junit-reporter.spec.ts b/packages/core/src/lib/cli/tests/junit-reporter.spec.ts new file mode 100644 index 00000000..a9b3e86b --- /dev/null +++ b/packages/core/src/lib/cli/tests/junit-reporter.spec.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, it, expect, beforeAll } from 'vitest'; +import { VirtualFs } from '../../fs/virtual-fs'; +import getFs, { useVirtualFs } from '../../fs/getFs'; +import { toFsPath } from '../../file-info/fs-path'; +import { SheriffViolations } from '../sheriff-violations'; +import { JunitReporter } from '../internal/reporter/junit/junit-reporter'; + +describe('JUnit reporter', () => { + let fs: VirtualFs; + beforeAll(() => { + useVirtualFs(); + fs = getFs() as VirtualFs; + }); + + beforeEach(() => { + fs.reset(); + fs.createDir('/project/customers/feature'); + fs.createDir('/project/shared/master-data'); + fs.createDir('/project/customers/ui'); + fs.createDir('/project/app/shared/form'); + }); + it('should create a xml-file in /.sheriff/project/violations.xml', () => { + const reporter = new JunitReporter({ + outputDir: '.sheriff', + projectName: 'project', + }); + const violations: SheriffViolations = { + totalEncapsulationViolations: 0, + totalViolatedFiles: 0, + totalDependencyRuleViolations: 2, + hasError: true, + violations: [ + { + filePath: '/project/customers/feature/feature.ts', + encapsulations: [], + dependencyRules: [], + dependencyRuleViolations: [ + { + rawImport: '@eternal/shared/master-data', + fromModulePath: toFsPath('/project/customers/feature'), + toModulePath: toFsPath('/project/shared/master-data'), + fromTag: 'domain:customers', + toTags: ['shared:master-data'], + }, + ], + }, + { + filePath: '/project/customers/ui/ui.ts', + encapsulations: [], + dependencyRules: [], + dependencyRuleViolations: [ + { + rawImport: '@eternal/shared/form', + fromModulePath: toFsPath('/project/customers/ui'), + toModulePath: toFsPath('/project/app/shared/form'), + fromTag: 'domain:customers', + toTags: ['shared:form'], + }, + ], + }, + ], + }; + reporter.createReport(violations); + + expect(fs.readFile('.sheriff/project/violations.xml')).toMatchSnapshot(); + }); +}); From 00012e6271ac473da6c5fd97c824cd2aac37c693 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 21:34:09 +0200 Subject: [PATCH 24/36] feat(core): add ReporterFormat type --- .../lib/cli/internal/reporter/supported-reporter-formats.ts | 2 ++ packages/core/src/lib/config/user-sheriff-config.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/lib/cli/internal/reporter/supported-reporter-formats.ts b/packages/core/src/lib/cli/internal/reporter/supported-reporter-formats.ts index 77c69747..6c1a89e2 100644 --- a/packages/core/src/lib/cli/internal/reporter/supported-reporter-formats.ts +++ b/packages/core/src/lib/cli/internal/reporter/supported-reporter-formats.ts @@ -1 +1,3 @@ +export type ReporterFormat = 'json' | 'junit'; + export const SUPPORTED_REPORTER_FORMATS = ['json', 'junit']; diff --git a/packages/core/src/lib/config/user-sheriff-config.ts b/packages/core/src/lib/config/user-sheriff-config.ts index 85bd0b58..7ce4adad 100644 --- a/packages/core/src/lib/config/user-sheriff-config.ts +++ b/packages/core/src/lib/config/user-sheriff-config.ts @@ -1,5 +1,6 @@ import { ModuleConfig } from './module-config'; import { DependencyRulesConfig } from './dependency-rules-config'; +import { ReporterFormat } from '../cli/internal/reporter/supported-reporter-formats'; /** * Exported by **sheriff.config.ts**. It is optional and should be located @@ -269,5 +270,5 @@ export interface UserSheriffConfig { /** * The reporters used to generate reports. */ - reporters?: string[]; + reporters?: ReporterFormat[]; } From 89c4177dd53f030069cb97dbfd189288099fbb29 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 21:35:57 +0200 Subject: [PATCH 25/36] refactor(core): rename SheriffViolations to ProjectViolation --- .../src/lib/cli/internal/reporter/json/json-reporter.ts | 4 ++-- .../src/lib/cli/internal/reporter/junit/junit-reporter.ts | 6 +++--- packages/core/src/lib/cli/internal/reporter/reporter.ts | 4 ++-- .../lib/cli/{sheriff-violations.ts => project-violation.ts} | 2 +- packages/core/src/lib/cli/tests/json-reporter.spec.ts | 4 ++-- packages/core/src/lib/cli/tests/junit-reporter.spec.ts | 4 ++-- packages/core/src/lib/cli/verify.ts | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) rename packages/core/src/lib/cli/{sheriff-violations.ts => project-violation.ts} (85%) diff --git a/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts b/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts index 164615b3..58c197cb 100644 --- a/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts +++ b/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts @@ -2,7 +2,7 @@ import { Reporter } from '../reporter'; import { cli } from '../../../cli'; import getFs from '../../../../fs/getFs'; -import { SheriffViolations } from '../../../sheriff-violations'; +import { ProjectViolation } from '../../../project-violation'; import { DEFAULT_PROJECT_NAME } from '../../get-entries-from-cli-or-config'; export class JsonReporter implements Reporter { @@ -11,7 +11,7 @@ export class JsonReporter implements Reporter { this.#options = options; } - createReport(validationResults: SheriffViolations): void { + createReport(validationResults: ProjectViolation): void { const fs = getFs(); const targetPath = this.#options.projectName === DEFAULT_PROJECT_NAME diff --git a/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts b/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts index d74d29e5..40cea2eb 100644 --- a/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts +++ b/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts @@ -1,4 +1,4 @@ -import { SheriffViolations } from '../../../sheriff-violations'; +import { ProjectViolation } from '../../../project-violation'; import getFs from '../../../../fs/getFs'; import { cli } from '../../../cli'; import { Reporter } from '../reporter'; @@ -11,7 +11,7 @@ export class JunitReporter implements Reporter { this.#options = options; } - createReport(validationResults: SheriffViolations) { + createReport(validationResults: ProjectViolation) { const fs = getFs(); const targetPath = this.#options.projectName === DEFAULT_PROJECT_NAME @@ -41,7 +41,7 @@ export class JunitReporter implements Reporter { return '.xml'; } - #generateXml(validationResults: SheriffViolations): string { + #generateXml(validationResults: ProjectViolation): string { const builder = junitBuilder(); const suite = builder.testsuite({ name: this.#options.projectName, diff --git a/packages/core/src/lib/cli/internal/reporter/reporter.ts b/packages/core/src/lib/cli/internal/reporter/reporter.ts index c18dec08..227e579f 100644 --- a/packages/core/src/lib/cli/internal/reporter/reporter.ts +++ b/packages/core/src/lib/cli/internal/reporter/reporter.ts @@ -1,5 +1,5 @@ -import { SheriffViolations } from '../../sheriff-violations'; +import { ProjectViolation } from '../../project-violation'; export interface Reporter { - createReport(validationResults: SheriffViolations): void; + createReport(validationResults: ProjectViolation): void; } diff --git a/packages/core/src/lib/cli/sheriff-violations.ts b/packages/core/src/lib/cli/project-violation.ts similarity index 85% rename from packages/core/src/lib/cli/sheriff-violations.ts rename to packages/core/src/lib/cli/project-violation.ts index 2772ee76..b542c33d 100644 --- a/packages/core/src/lib/cli/sheriff-violations.ts +++ b/packages/core/src/lib/cli/project-violation.ts @@ -1,6 +1,6 @@ import { Violation } from './verify'; -export type SheriffViolations = { +export type ProjectViolation = { totalDependencyRuleViolations: number; totalEncapsulationViolations: number; totalViolatedFiles: number; diff --git a/packages/core/src/lib/cli/tests/json-reporter.spec.ts b/packages/core/src/lib/cli/tests/json-reporter.spec.ts index b7a7c727..9d9c6018 100644 --- a/packages/core/src/lib/cli/tests/json-reporter.spec.ts +++ b/packages/core/src/lib/cli/tests/json-reporter.spec.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, it, expect, beforeAll } from 'vitest'; import { VirtualFs } from '../../fs/virtual-fs'; import getFs, { useVirtualFs } from '../../fs/getFs'; import { toFsPath } from '../../file-info/fs-path'; -import { SheriffViolations } from '../sheriff-violations'; +import { ProjectViolation } from '../project-violation'; import { JsonReporter } from '../internal/reporter/json/json-reporter'; describe('json reporter', () => { @@ -24,7 +24,7 @@ describe('json reporter', () => { outputDir: '.sheriff', projectName: 'project', }); - const violations: SheriffViolations = { + const violations: ProjectViolation = { totalEncapsulationViolations: 0, totalViolatedFiles: 0, totalDependencyRuleViolations: 2, diff --git a/packages/core/src/lib/cli/tests/junit-reporter.spec.ts b/packages/core/src/lib/cli/tests/junit-reporter.spec.ts index a9b3e86b..5e6c1bc6 100644 --- a/packages/core/src/lib/cli/tests/junit-reporter.spec.ts +++ b/packages/core/src/lib/cli/tests/junit-reporter.spec.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, it, expect, beforeAll } from 'vitest'; import { VirtualFs } from '../../fs/virtual-fs'; import getFs, { useVirtualFs } from '../../fs/getFs'; import { toFsPath } from '../../file-info/fs-path'; -import { SheriffViolations } from '../sheriff-violations'; +import { ProjectViolation } from '../project-violation'; import { JunitReporter } from '../internal/reporter/junit/junit-reporter'; describe('JUnit reporter', () => { @@ -24,7 +24,7 @@ describe('JUnit reporter', () => { outputDir: '.sheriff', projectName: 'project', }); - const violations: SheriffViolations = { + const violations: ProjectViolation = { totalEncapsulationViolations: 0, totalViolatedFiles: 0, totalDependencyRuleViolations: 2, diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index eabbb922..edd08157 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -12,7 +12,7 @@ import { } from './internal/get-entries-from-cli-or-config'; import { logInfoForMissingSheriffConfig } from './internal/log-info-for-missing-sheriff-config'; import { reporterFactory } from './internal/reporter/reporter-factory'; -import { SheriffViolations } from './sheriff-violations'; +import { ProjectViolation } from './project-violation'; import { ProjectInfo } from '../main/init'; import { Entry } from './internal/entry'; @@ -179,7 +179,7 @@ function createReports( }); if (projectValidation) { - const violations: SheriffViolations = { + const violations: ProjectViolation = { hasError: projectValidation.hasError, totalDependencyRuleViolations: projectValidation.dependencyRulesCount, totalEncapsulationViolations: projectValidation.encapsulationsCount, From 9ad6e10b9ab6cf81b833b2f72701b9bcd480e0a0 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 21:39:26 +0200 Subject: [PATCH 26/36] chore(core): add tests --- .../lib/cli/tests/reporter-factory.spec.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 packages/core/src/lib/cli/tests/reporter-factory.spec.ts diff --git a/packages/core/src/lib/cli/tests/reporter-factory.spec.ts b/packages/core/src/lib/cli/tests/reporter-factory.spec.ts new file mode 100644 index 00000000..71792965 --- /dev/null +++ b/packages/core/src/lib/cli/tests/reporter-factory.spec.ts @@ -0,0 +1,91 @@ +import { describe, it, expect } from 'vitest'; +import { reporterFactory } from '../internal/reporter/reporter-factory'; +import { JsonReporter } from '../internal/reporter/json/json-reporter'; +import { JunitReporter } from '../internal/reporter/junit/junit-reporter'; + +describe('ReporterFactory', () => { + const defaultOptions = { + outputDir: 'test-reports', + projectName: 'test-project', + }; + + it('should return empty array when no reporter formats are provided', () => { + const reporters = reporterFactory({ + ...defaultOptions, + reporterFormats: [], + }); + + expect(reporters).toEqual([]); + }); + + it('should create JsonReporter when json format is specified', () => { + const reporters = reporterFactory({ + ...defaultOptions, + reporterFormats: ['json'], + }); + + expect(reporters).toHaveLength(1); + expect(reporters[0]).toBeInstanceOf(JsonReporter); + }); + + it('should create JunitReporter when junit format is specified', () => { + const reporters = reporterFactory({ + ...defaultOptions, + reporterFormats: ['junit'], + }); + + expect(reporters).toHaveLength(1); + expect(reporters[0]).toBeInstanceOf(JunitReporter); + }); + + it('should create multiple reporters when multiple formats are specified', () => { + const reporters = reporterFactory({ + ...defaultOptions, + reporterFormats: ['json', 'junit'], + }); + + expect(reporters).toHaveLength(2); + expect(reporters[0]).toBeInstanceOf(JsonReporter); + expect(reporters[1]).toBeInstanceOf(JunitReporter); + }); + + it('should ignore unknown reporter formats', () => { + const reporters = reporterFactory({ + ...defaultOptions, + reporterFormats: ['json', 'unknown-format', 'junit'], + }); + + expect(reporters).toHaveLength(2); + expect(reporters[0]).toBeInstanceOf(JsonReporter); + expect(reporters[1]).toBeInstanceOf(JunitReporter); + }); + + it('should handle duplicate reporter formats by creating multiple instances', () => { + const reporters = reporterFactory({ + ...defaultOptions, + reporterFormats: ['json', 'json'], + }); + + expect(reporters).toHaveLength(2); + expect(reporters[0]).toBeInstanceOf(JsonReporter); + expect(reporters[1]).toBeInstanceOf(JsonReporter); + }); + + it('should maintain order of reporters based on format array order', () => { + const reporters1 = reporterFactory({ + ...defaultOptions, + reporterFormats: ['json', 'junit'], + }); + + const reporters2 = reporterFactory({ + ...defaultOptions, + reporterFormats: ['junit', 'json'], + }); + + expect(reporters1[0]).toBeInstanceOf(JsonReporter); + expect(reporters1[1]).toBeInstanceOf(JunitReporter); + + expect(reporters2[0]).toBeInstanceOf(JunitReporter); + expect(reporters2[1]).toBeInstanceOf(JsonReporter); + }); +}); From a6ab0ea7944aab81d301e749b4d8ae14b929b9fa Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 21:49:39 +0200 Subject: [PATCH 27/36] refactor(core): remove console..og --- packages/core/src/lib/cli/verify.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index edd08157..b8f165aa 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -32,7 +32,6 @@ type ProjectValidation = { }; export function verify(args: string[]) { - console.log('verify ', args); const fs = getFs(); const projectEntries = getEntriesFromCliOrConfig(args[0]); logInfoForMissingSheriffConfig(projectEntries[0].entry); From 30e5f78050707efcf691ba3016d5a580032c461b Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 21:53:01 +0200 Subject: [PATCH 28/36] chore(core): add tests --- .../core/src/lib/cli/tests/verify.spec.ts | 147 +++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/packages/core/src/lib/cli/tests/verify.spec.ts b/packages/core/src/lib/cli/tests/verify.spec.ts index f8fde7fd..ad27a15f 100644 --- a/packages/core/src/lib/cli/tests/verify.spec.ts +++ b/packages/core/src/lib/cli/tests/verify.spec.ts @@ -1,10 +1,12 @@ -import { beforeEach, describe, expect, vitest, it } from 'vitest'; +import { beforeEach, describe, expect, vitest, it, vi } from 'vitest'; import { createProject } from '../../test/project-creator'; import { tsConfig } from '../../test/fixtures/ts-config'; import { main } from '../main'; import { sheriffConfig } from '../../test/project-configurator'; import { verifyCliWrappers } from './verify-cli-wrapper'; import { mockCli } from './helpers/mock-cli'; +import { JsonReporter } from '../internal/reporter/json/json-reporter'; +import { JunitReporter } from '../internal/reporter/junit/junit-reporter'; describe('verify', () => { beforeEach(() => { @@ -198,4 +200,147 @@ describe('verify', () => { expect(allLogs()).toMatchSnapshot('logs.log'); }); }); + + describe('Reporter integration', () => { + it('should create reports for single project workspace', () => { + mockCli(); + + // Spy on reporter createReport methods + const jsonReporterSpy = vi.spyOn(JsonReporter.prototype, 'createReport'); + const junitReporterSpy = vi.spyOn( + JunitReporter.prototype, + 'createReport', + ); + + createProject({ + 'tsconfig.json': tsConfig(), + 'sheriff.config.ts': sheriffConfig({ + reporters: ['json', 'junit'], + modules: { + 'src/customers': ['customers'], + 'src/holidays': ['holidays'], + }, + depRules: { + root: ['customers', 'holidays'], + customers: [], + holidays: [], + }, + }), + src: { + 'main.ts': ['./holidays', './customers'], + holidays: { + 'index.ts': ['./holidays.component'], + 'holidays.component.ts': ['../customers'], // violation + }, + customers: { 'index.ts': [] }, + }, + }); + + main('verify', 'src/main.ts'); + + expect(jsonReporterSpy).toHaveBeenCalledTimes(1); + expect(junitReporterSpy).toHaveBeenCalledTimes(1); + }); + + it('should create reports for multi-project workspace', () => { + mockCli(); + + const jsonReporterSpy = vi.spyOn(JsonReporter.prototype, 'createReport'); + const junitReporterSpy = vi.spyOn( + JunitReporter.prototype, + 'createReport', + ); + + createProject({ + 'tsconfig.json': tsConfig(), + 'sheriff.config.ts': sheriffConfig({ + entryPoints: { + 'project-i': 'projects/project-i/src/main.ts', + 'project-ii': 'projects/project-ii/src/main.ts', + }, + depRules: {}, + reporters: ['json', 'junit'], + }), + projects: { + 'project-i': { + src: { + 'main.ts': [], + 'app.ts': [], + }, + }, + 'project-ii': { + src: { + 'main.ts': [], + 'app.ts': [], + }, + }, + }, + }); + + main('verify', 'project-i,project-ii'); + + expect(jsonReporterSpy).toHaveBeenCalledTimes(2); + expect(junitReporterSpy).toHaveBeenCalledTimes(2); + }); + + it('should not create reports when no reporters are configured', () => { + mockCli(); + + const jsonReporterSpy = vi.spyOn(JsonReporter.prototype, 'createReport'); + const junitReporterSpy = vi.spyOn( + JunitReporter.prototype, + 'createReport', + ); + + createProject({ + 'tsconfig.json': tsConfig(), + 'sheriff.config.ts': sheriffConfig({ + modules: { + 'src/customers': ['customers'], + }, + depRules: { + root: ['customers'], + customers: [], + }, + }), + src: { + 'main.ts': ['./customers'], + customers: { 'index.ts': [] }, + }, + }); + + main('verify', 'src/main.ts'); + + expect(jsonReporterSpy).not.toHaveBeenCalled(); + expect(junitReporterSpy).not.toHaveBeenCalled(); + }); + + it('should create reports even when no violations found', () => { + mockCli(); + + const jsonReporterSpy = vi.spyOn(JsonReporter.prototype, 'createReport'); + + createProject({ + 'tsconfig.json': tsConfig(), + 'sheriff.config.ts': sheriffConfig({ + reporters: ['json'], + modules: { + 'src/customers': ['customers'], + }, + depRules: { + root: ['customers'], + customers: [], + }, + }), + src: { + 'main.ts': ['./customers'], + customers: { 'index.ts': [] }, + }, + }); + + main('verify', 'src/main.ts'); + + expect(jsonReporterSpy).toHaveBeenCalledTimes(1); + }); + }); }); From 8c21b1da55af1152aec85f9edc3bdd95a8ee8568 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sun, 27 Jul 2025 22:08:49 +0200 Subject: [PATCH 29/36] docs(core): document Reports --- docs/docs/reports.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/docs/reports.md diff --git a/docs/docs/reports.md b/docs/docs/reports.md new file mode 100644 index 00000000..fa67acad --- /dev/null +++ b/docs/docs/reports.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 7 +title: Creating violation reports +displayed_sidebar: tutorialSidebar +--- + +`Sheriff` can generate violation reports in various formats, which can be useful for integrating with CI/CD pipelines or for manual review. The reports can be generated in JSON, JUnit format. + +The reports are generated when `sheriff verify` is executed and `reporters` are configured in the `sheriff.config.ts` file. + +## Defining the report format +To define the report format, you can use the `reportFormat` property in your `sheriff.config.ts` file. This property accepts an array of report formats which should be used: `reporters: ['json']` + +## Custom directory where reports are written to +By default reports are written to `./sheriff/reports` relative to the project root. The directory can be customized by setting the `reportsDirectory`- property in the `sheriff.config.ts`. From 9744aab7b9d916f5e9f765c4ffeaf9763b43fcdf Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 28 Jul 2025 06:52:03 +0200 Subject: [PATCH 30/36] refactor(core): centralize shared logic for reporter --- .../internal/reporter/json/json-reporter.ts | 38 ++++----------- .../internal/reporter/junit/junit-reporter.ts | 39 ++++----------- .../reporter/utils/reporter-options.ts | 4 ++ .../internal/reporter/utils/save-report.ts | 47 +++++++++++++++++++ 4 files changed, 67 insertions(+), 61 deletions(-) create mode 100644 packages/core/src/lib/cli/internal/reporter/utils/reporter-options.ts create mode 100644 packages/core/src/lib/cli/internal/reporter/utils/save-report.ts diff --git a/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts b/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts index 58c197cb..1dff55ee 100644 --- a/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts +++ b/packages/core/src/lib/cli/internal/reporter/json/json-reporter.ts @@ -1,42 +1,20 @@ import { Reporter } from '../reporter'; import { cli } from '../../../cli'; -import getFs from '../../../../fs/getFs'; - import { ProjectViolation } from '../../../project-violation'; -import { DEFAULT_PROJECT_NAME } from '../../get-entries-from-cli-or-config'; +import { saveReport } from '../utils/save-report'; +import { ReporterOptions } from '../utils/reporter-options'; export class JsonReporter implements Reporter { - #options: { outputDir: string; projectName: string }; - constructor(options: { outputDir: string; projectName: string }) { + #options: ReporterOptions; + + constructor(options: ReporterOptions) { this.#options = options; } createReport(validationResults: ProjectViolation): void { - const fs = getFs(); - const targetPath = - this.#options.projectName === DEFAULT_PROJECT_NAME - ? fs.join( - this.#options.outputDir, - 'violations' + this.#getReportExtension(), - ) - : fs.join( - this.#options.outputDir, - this.#options.projectName, - 'violations' + this.#getReportExtension(), - ); - cli.log(`Creating JSON-export`); - if (this.#options.projectName === DEFAULT_PROJECT_NAME) { - fs.createDir(fs.join(this.#options.outputDir)); - } else { - fs.createDir( - fs.join(this.#options.outputDir, this.#options.projectName!), - ); - } - - fs.writeFile(targetPath, JSON.stringify(validationResults, null, 2)); - } + cli.log(`Creating JSON report`); - #getReportExtension(): string { - return '.json'; + const content = JSON.stringify(validationResults, null, 2); + saveReport(this.#options, 'violations', '.json', content); } } diff --git a/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts b/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts index 40cea2eb..26be5247 100644 --- a/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts +++ b/packages/core/src/lib/cli/internal/reporter/junit/junit-reporter.ts @@ -1,44 +1,22 @@ import { ProjectViolation } from '../../../project-violation'; -import getFs from '../../../../fs/getFs'; import { cli } from '../../../cli'; import { Reporter } from '../reporter'; import { junitBuilder } from './internal/junit-report-builder'; -import { DEFAULT_PROJECT_NAME } from '../../get-entries-from-cli-or-config'; +import { saveReport } from '../utils/save-report'; +import { ReporterOptions } from '../utils/reporter-options'; export class JunitReporter implements Reporter { - #options: { outputDir: string; projectName: string }; - constructor(options: { outputDir: string; projectName: string }) { + #options: ReporterOptions; + + constructor(options: ReporterOptions) { this.#options = options; } - createReport(validationResults: ProjectViolation) { - const fs = getFs(); - const targetPath = - this.#options.projectName === DEFAULT_PROJECT_NAME - ? fs.join( - this.#options.outputDir, - 'violations' + this.#getReportExtension(), - ) - : fs.join( - this.#options.outputDir, - this.#options.projectName, - 'violations' + this.#getReportExtension(), - ); - cli.log(`Creating JUnit XML report`); + createReport(validationResults: ProjectViolation): void { + cli.log(`Creating JUnit report`); - if (this.#options.projectName === DEFAULT_PROJECT_NAME) { - fs.createDir(fs.join(this.#options.outputDir)); - } else { - fs.createDir( - fs.join(this.#options.outputDir, this.#options.projectName!), - ); - } const xmlContent = this.#generateXml(validationResults); - fs.writeFile(targetPath, xmlContent); - } - - #getReportExtension(): string { - return '.xml'; + saveReport(this.#options, 'violations', '.xml', xmlContent); } #generateXml(validationResults: ProjectViolation): string { @@ -72,7 +50,6 @@ export class JunitReporter implements Reporter { suite.addTestCase({ modulePath: violation.filePath, name: 'dependency-rule', - // TODO in verify we already build the message failureMessage: `module ${fromModulePath} cannot access ${toModulePath}. Tag ${depViolation.fromTag} has no clearance for tags ${depViolation.toTags.join(',')}`, fromTag: depViolation.fromTag, toTags: depViolation.toTags.join(','), diff --git a/packages/core/src/lib/cli/internal/reporter/utils/reporter-options.ts b/packages/core/src/lib/cli/internal/reporter/utils/reporter-options.ts new file mode 100644 index 00000000..535116af --- /dev/null +++ b/packages/core/src/lib/cli/internal/reporter/utils/reporter-options.ts @@ -0,0 +1,4 @@ +export interface ReporterOptions { + outputDir: string; + projectName: string; +} diff --git a/packages/core/src/lib/cli/internal/reporter/utils/save-report.ts b/packages/core/src/lib/cli/internal/reporter/utils/save-report.ts new file mode 100644 index 00000000..a29506a1 --- /dev/null +++ b/packages/core/src/lib/cli/internal/reporter/utils/save-report.ts @@ -0,0 +1,47 @@ +import getFs from '../../../../fs/getFs'; +import { DEFAULT_PROJECT_NAME } from '../../get-entries-from-cli-or-config'; +import { ReporterOptions } from './reporter-options'; + +/** + * Saves content to a file, creating the directory structure if needed + * @param options Reporter options containing outputDir and projectName + * @param fileName The base filename for the report + * @param extension The file extension including the dot + * @param content The content to write to the file + */ +export function saveReport( + options: ReporterOptions, + fileName: string, + extension: string, + content: string, +): void { + const fs = getFs(); + + createReportDirectory(options); + const targetPath = getReportTargetPath(options, fileName, extension); + fs.writeFile(targetPath, content); +} + +function getReportTargetPath( + options: ReporterOptions, + fileName: string, + extension: string, +): string { + const fs = getFs(); + + if (options.projectName === DEFAULT_PROJECT_NAME) { + return fs.join(options.outputDir, fileName + extension); + } + + return fs.join(options.outputDir, options.projectName, fileName + extension); +} + +function createReportDirectory(options: ReporterOptions): void { + const fs = getFs(); + + if (options.projectName === DEFAULT_PROJECT_NAME) { + fs.createDir(options.outputDir); + } else { + fs.createDir(fs.join(options.outputDir, options.projectName)); + } +} From 31042f81d93ce085bb3a792a2baead7d08a66a0e Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 28 Jul 2025 07:16:14 +0200 Subject: [PATCH 31/36] fix(docs): fix typo --- docs/docs/reports.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/reports.md b/docs/docs/reports.md index fa67acad..4b1a9f2a 100644 --- a/docs/docs/reports.md +++ b/docs/docs/reports.md @@ -8,8 +8,8 @@ displayed_sidebar: tutorialSidebar The reports are generated when `sheriff verify` is executed and `reporters` are configured in the `sheriff.config.ts` file. -## Defining the report format -To define the report format, you can use the `reportFormat` property in your `sheriff.config.ts` file. This property accepts an array of report formats which should be used: `reporters: ['json']` +## Defining the reporters +To define the report format, you can use the `reporters` property in your `sheriff.config.ts` file. This property accepts an array of report formats which should be used: `reporters: ['json']` ## Custom directory where reports are written to By default reports are written to `./sheriff/reports` relative to the project root. The directory can be customized by setting the `reportsDirectory`- property in the `sheriff.config.ts`. From 5623c3b6557f17366fdf3a0bed9ae61c30fa98df Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 28 Jul 2025 07:20:41 +0200 Subject: [PATCH 32/36] fix(core): do not push deep-import issue when encapsulation issue is detected --- .../junit/internal/junit-report-builder.ts | 21 +++++++------------ .../cli/tests/junit-report-builder.spec.ts | 3 --- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts b/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts index 506cc5cf..55fd619d 100644 --- a/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts +++ b/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts @@ -36,7 +36,8 @@ class JUnitTestSuite implements TestSuite { constructor(options: TestSuiteOptions) { this.name = options.name; - this.totalDependencyRulesViolations = options.totalDependencyRulesViolations; + this.totalDependencyRulesViolations = + options.totalDependencyRulesViolations; this.totalEncapsulationViolations = options.totalEncapsulationViolations; this.totalViolatedFiles = options.totalViolatedFiles; this.hasError = options.hasError; @@ -44,16 +45,6 @@ class JUnitTestSuite implements TestSuite { addTestCase(testCase: TestCase): void { this.testCases.push(testCase); - - // When adding an encapsulation violation, also add a deep-import violation for the same file - if (testCase.name === 'encapsulation') { - const deepImportCase: TestCase = { - modulePath: testCase.modulePath, - name: 'deep-import', - failureMessage: `.${testCase.modulePath} is a deep import from a barrel module. Use the module's barrel file (index.ts) instead.` - }; - this.testCases.push(deepImportCase); - } } } @@ -87,7 +78,10 @@ class JUnitReportBuilder implements JUnitBuilder { report += ` \n`; for (const testCase of testSuite.testCases) { - const attributes = [`modulePath="${testCase.modulePath}"`, `name="${testCase.name}"`]; + const attributes = [ + `modulePath="${testCase.modulePath}"`, + `name="${testCase.name}"`, + ]; if (testCase.fromTag) { attributes.push(`fromTag="${testCase.fromTag}"`); @@ -102,7 +96,8 @@ class JUnitReportBuilder implements JUnitBuilder { attributes.push(`toModulePath="${testCase.toModulePath}"`); } - const hasSpaceBeforeClose = testCase.name === 'encapsulation' ? ' ' : ''; + const hasSpaceBeforeClose = + testCase.name === 'encapsulation' ? ' ' : ''; report += ` \n`; report += ` \n`; report += ` \n`; diff --git a/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts b/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts index b0646757..4db20fd9 100644 --- a/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts +++ b/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts @@ -59,9 +59,6 @@ describe('JUnitReportBuilder', () => { - - - From 413ad1cfd22ba9c203198d2b4aac4f8e0107c80a Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 28 Jul 2025 07:42:59 +0200 Subject: [PATCH 33/36] refactor(core): add reporter registry --- .../cli/internal/reporter/reporter-factory.ts | 28 ++++++++++++------- .../reporter/supported-reporter-formats.ts | 4 ++- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts b/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts index cfded3ec..3e23851d 100644 --- a/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts +++ b/packages/core/src/lib/cli/internal/reporter/reporter-factory.ts @@ -2,24 +2,28 @@ import { JsonReporter } from './json/json-reporter'; import { Reporter } from './reporter'; import { JunitReporter } from './junit/junit-reporter'; +type ReporterConstructor = new (options: { + outputDir: string; + projectName: string; +}) => Reporter; + +const REPORTER_REGISTRY = new Map([ + ['json', JsonReporter], + ['junit', JunitReporter], +]); + export function reporterFactory(options: { reporterFormats: string[]; outputDir: string; projectName: string; }): Reporter[] { const reporters: Reporter[] = []; + options.reporterFormats.forEach((format) => { - if (format === 'json') { - reporters.push( - new JsonReporter({ - outputDir: options.outputDir, - projectName: options.projectName, - }), - ); - } - if (format === 'junit') { + const ReporterClass = REPORTER_REGISTRY.get(format); + if (ReporterClass) { reporters.push( - new JunitReporter({ + new ReporterClass({ outputDir: options.outputDir, projectName: options.projectName, }), @@ -29,3 +33,7 @@ export function reporterFactory(options: { return reporters; } + +export function getRegisteredFormats(): string[] { + return Array.from(REPORTER_REGISTRY.keys()); +} diff --git a/packages/core/src/lib/cli/internal/reporter/supported-reporter-formats.ts b/packages/core/src/lib/cli/internal/reporter/supported-reporter-formats.ts index 6c1a89e2..5ca9b3a8 100644 --- a/packages/core/src/lib/cli/internal/reporter/supported-reporter-formats.ts +++ b/packages/core/src/lib/cli/internal/reporter/supported-reporter-formats.ts @@ -1,3 +1,5 @@ +import { getRegisteredFormats } from './reporter-factory'; + export type ReporterFormat = 'json' | 'junit'; -export const SUPPORTED_REPORTER_FORMATS = ['json', 'junit']; +export const SUPPORTED_REPORTER_FORMATS = getRegisteredFormats(); From 92155826dcd3ae6dcf8e49cccbbf800b8db9b6cf Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 28 Jul 2025 07:49:23 +0200 Subject: [PATCH 34/36] fix(core): spacing issue when creating JUnit report --- .../internal/reporter/junit/internal/junit-report-builder.ts | 4 +--- packages/core/src/lib/cli/tests/junit-report-builder.spec.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts b/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts index 55fd619d..e63e9d2a 100644 --- a/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts +++ b/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts @@ -96,9 +96,7 @@ class JUnitReportBuilder implements JUnitBuilder { attributes.push(`toModulePath="${testCase.toModulePath}"`); } - const hasSpaceBeforeClose = - testCase.name === 'encapsulation' ? ' ' : ''; - report += ` \n`; + report += ` \n`; report += ` \n`; report += ` \n`; } diff --git a/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts b/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts index 4db20fd9..ef52ca85 100644 --- a/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts +++ b/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts @@ -56,7 +56,7 @@ describe('JUnitReportBuilder', () => { const expectedResult = ` - + From 57de38c9a899bf0c25c2999b3a6a359475c1bd5b Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 28 Jul 2025 08:25:06 +0200 Subject: [PATCH 35/36] refactor(core): rename property validationsMap to ruleViolations --- packages/core/src/lib/cli/verify.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index b8f165aa..1957bac2 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -28,7 +28,7 @@ type ProjectValidation = { encapsulationsCount: number; filesCount: number; hasError: boolean; - validationsMap: Violation[]; + ruleViolations: Violation[]; }; export function verify(args: string[]) { @@ -51,7 +51,7 @@ export function verify(args: string[]) { encapsulationsCount: 0, filesCount: 0, hasError: false, - validationsMap: [], + ruleViolations: [], }; projectValidations.set(projectName, validation); @@ -81,7 +81,7 @@ export function verify(args: string[]) { ); const relativePath = fs.relativeTo(fs.cwd(), fileInfo.path); - projectValidation.validationsMap.push({ + projectValidation.ruleViolations.push({ filePath: relativePath, encapsulations, dependencyRules, @@ -119,7 +119,7 @@ export function verify(args: string[]) { encapsulations, dependencyRules, filePath, - } of validation.validationsMap) { + } of validation.ruleViolations) { cli.log('|-- ' + filePath); if (encapsulations.length > 0) { cli.log('| |-- Encapsulation Violations'); @@ -183,7 +183,7 @@ function createReports( totalDependencyRuleViolations: projectValidation.dependencyRulesCount, totalEncapsulationViolations: projectValidation.encapsulationsCount, totalViolatedFiles: projectValidation.filesCount, - violations: projectValidation.validationsMap, + violations: projectValidation.ruleViolations, }; reporters.forEach((reporter) => { From e9f23cd65e4c313ee141b84ff0298afd9942a2c4 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 28 Jul 2025 09:59:21 +0200 Subject: [PATCH 36/36] fix(core): remove uneccessary whitespace in XML-report --- .../internal/reporter/junit/internal/junit-report-builder.ts | 2 +- .../lib/cli/tests/__snapshots__/junit-reporter.spec.ts.snap | 2 +- packages/core/src/lib/cli/tests/junit-report-builder.spec.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts b/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts index e63e9d2a..15999538 100644 --- a/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts +++ b/packages/core/src/lib/cli/internal/reporter/junit/internal/junit-report-builder.ts @@ -75,7 +75,7 @@ class JUnitReportBuilder implements JUnitBuilder { for (const suite of this.testSuites) { const testSuite = suite as JUnitTestSuite; - report += ` \n`; + report += ` \n`; for (const testCase of testSuite.testCases) { const attributes = [ diff --git a/packages/core/src/lib/cli/tests/__snapshots__/junit-reporter.spec.ts.snap b/packages/core/src/lib/cli/tests/__snapshots__/junit-reporter.spec.ts.snap index 3121eb7d..64ebdbd7 100644 --- a/packages/core/src/lib/cli/tests/__snapshots__/junit-reporter.spec.ts.snap +++ b/packages/core/src/lib/cli/tests/__snapshots__/junit-reporter.spec.ts.snap @@ -3,7 +3,7 @@ exports[`JUnit reporter > should create a xml-file in /.sheriff/project/violations.xml 1`] = ` " - + diff --git a/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts b/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts index ef52ca85..300a5520 100644 --- a/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts +++ b/packages/core/src/lib/cli/tests/junit-report-builder.spec.ts @@ -21,7 +21,7 @@ describe('JUnitReportBuilder', () => { }); const expectedResult = ` - + `; @@ -55,7 +55,7 @@ describe('JUnitReportBuilder', () => { const expectedResult = ` - +