@ovh-ux/manager-static-analysis-kit 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +59 -69
  3. package/bin/cli-path-config.js +31 -0
  4. package/bin/code-duplication-cli.js +96 -0
  5. package/bin/code-duplication-tests-cli.js +68 -0
  6. package/bin/lint-cli.js +1 -1
  7. package/bin/perf-budgets-cli.js +58 -297
  8. package/bin/perf-budgets-tests-cli.js +1 -3
  9. package/bin/static-dynamic-quality-checks.js +138 -0
  10. package/bin/tests-coverage-cli-tests.js +67 -0
  11. package/bin/tests-coverage-cli.js +135 -0
  12. package/bin/types-coverage-cli.js +127 -0
  13. package/bin/types-coverage-tests-cli.js +68 -0
  14. package/bin/utils/apps-utils.js +64 -0
  15. package/bin/utils/args-parse-utils.js +222 -0
  16. package/bin/utils/file-utils.js +52 -0
  17. package/bin/utils/log-utils.js +29 -0
  18. package/bin/utils/runner-utils.js +85 -0
  19. package/bin/utils/turbo-utils.js +139 -0
  20. package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.d.ts +6 -0
  21. package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.d.ts.map +1 -0
  22. package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.js +240 -0
  23. package/dist/adapters/code-duplication/types/CodeDuplication.d.ts +60 -0
  24. package/dist/adapters/code-duplication/types/CodeDuplication.d.ts.map +1 -0
  25. package/dist/adapters/code-duplication/types/CodeDuplication.js +1 -0
  26. package/dist/adapters/eslint/rules/eslint-prettier.js +1 -1
  27. package/dist/adapters/html-a11y-validation/helpers/html-a11y-tests-setup.d.ts +62 -0
  28. package/dist/adapters/html-a11y-validation/helpers/html-a11y-tests-setup.d.ts.map +1 -0
  29. package/dist/adapters/html-a11y-validation/helpers/html-a11y-tests-setup.js +73 -0
  30. package/dist/adapters/html-w3c-validation/helpers/html-w3c-tests-setup.d.ts +19 -0
  31. package/dist/adapters/html-w3c-validation/helpers/html-w3c-tests-setup.d.ts.map +1 -0
  32. package/dist/adapters/html-w3c-validation/helpers/html-w3c-tests-setup.js +32 -0
  33. package/dist/adapters/html-w3c-validation/helpers/html-w3c-validator.d.ts +4 -0
  34. package/dist/adapters/html-w3c-validation/helpers/html-w3c-validator.d.ts.map +1 -0
  35. package/dist/adapters/html-w3c-validation/helpers/html-w3c-validator.js +89 -0
  36. package/dist/adapters/html-w3c-validation/types/HtmlW3CValidator.d.ts +6 -0
  37. package/dist/adapters/html-w3c-validation/types/HtmlW3CValidator.d.ts.map +1 -0
  38. package/dist/adapters/html-w3c-validation/types/HtmlW3CValidator.js +1 -0
  39. package/dist/adapters/html-w3c-validation/types/globals/vnu-jar.d.ts +5 -0
  40. package/dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.d.ts +1 -1
  41. package/dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.d.ts.map +1 -1
  42. package/dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.js +2 -3
  43. package/dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.d.ts +4 -0
  44. package/dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.d.ts.map +1 -0
  45. package/dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.js +192 -0
  46. package/dist/adapters/tests-coverage/types/TestsCoverage.d.ts +36 -0
  47. package/dist/adapters/tests-coverage/types/TestsCoverage.d.ts.map +1 -0
  48. package/dist/adapters/tests-coverage/types/TestsCoverage.js +6 -0
  49. package/dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.d.ts +4 -0
  50. package/dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.d.ts.map +1 -0
  51. package/dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.js +158 -0
  52. package/dist/adapters/types-coverage/types/TypesCoverage.d.ts +30 -0
  53. package/dist/adapters/types-coverage/types/TypesCoverage.d.ts.map +1 -0
  54. package/dist/adapters/types-coverage/types/TypesCoverage.js +1 -0
  55. package/dist/configs/architecture-analysis-config.d.ts +5 -0
  56. package/dist/configs/architecture-analysis-config.d.ts.map +1 -0
  57. package/dist/configs/{madge-config.js → architecture-analysis-config.js} +1 -1
  58. package/dist/configs/code-duplication-config.d.ts +21 -0
  59. package/dist/configs/code-duplication-config.d.ts.map +1 -0
  60. package/dist/configs/code-duplication-config.js +21 -0
  61. package/dist/configs/{prettier-config.d.ts → code-formatter-config.d.ts} +1 -1
  62. package/dist/configs/code-formatter-config.d.ts.map +1 -0
  63. package/dist/configs/file-globs-config.d.ts +0 -3
  64. package/dist/configs/file-globs-config.d.ts.map +1 -1
  65. package/dist/configs/file-globs-config.js +0 -3
  66. package/dist/configs/naming-conventions-config.d.ts.map +1 -1
  67. package/dist/configs/naming-conventions-config.js +2 -0
  68. package/dist/configs/shared-exclusion-patterns.d.ts.map +1 -1
  69. package/dist/configs/shared-exclusion-patterns.js +3 -1
  70. package/dist/configs/tests-coverage-config.d.ts +12 -0
  71. package/dist/configs/tests-coverage-config.d.ts.map +1 -0
  72. package/dist/configs/tests-coverage-config.js +11 -0
  73. package/dist/configs/types-coverage-config.d.ts +19 -0
  74. package/dist/configs/types-coverage-config.d.ts.map +1 -0
  75. package/dist/configs/types-coverage-config.js +27 -0
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +32 -7
  78. package/bin/cli-utils.js +0 -56
  79. package/dist/configs/jscpd-config.d.ts +0 -7
  80. package/dist/configs/jscpd-config.d.ts.map +0 -1
  81. package/dist/configs/jscpd-config.js +0 -7
  82. package/dist/configs/madge-config.d.ts +0 -5
  83. package/dist/configs/madge-config.d.ts.map +0 -1
  84. package/dist/configs/prettier-config.d.ts.map +0 -1
  85. /package/dist/configs/{prettier-config.js → code-formatter-config.js} +0 -0
@@ -0,0 +1,19 @@
1
+ import "vitest";
2
+
3
+ declare module "vitest" {
4
+ interface Assertion<T = any> {
5
+ /**
6
+ * Assert that a string of HTML is valid according to the W3C Nu Html Checker.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * await expect("<!doctype html><html><head></head><body></body></html>").toBeValidHtml();
11
+ * ```
12
+ */
13
+ toBeValidHtml(): Promise<void>;
14
+ }
15
+
16
+ interface AsymmetricMatchersContaining {
17
+ toBeValidHtml(): Promise<void>;
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-w3c-tests-setup.d.ts","sourceRoot":"","sources":["../../../../src/adapters/html-w3c-validation/helpers/html-w3c-tests-setup.ts"],"names":[],"mappings":""}
@@ -0,0 +1,32 @@
1
+ import { expect } from 'vitest';
2
+ import { validateHtmlString } from './html-w3c-validator.js';
3
+ function asDocument(html) {
4
+ const hasDoctype = /<!doctype\s+html/i.test(html);
5
+ const hasHtmlTag = /<html[\s>]/i.test(html);
6
+ if (hasDoctype || hasHtmlTag)
7
+ return html;
8
+ return [
9
+ '<!doctype html>',
10
+ '<html lang="en">',
11
+ '<head><meta charset="utf-8"><title>Test</title></head>',
12
+ '<body>',
13
+ html,
14
+ '</body></html>',
15
+ ].join('');
16
+ }
17
+ expect.extend({
18
+ async toBeValidHtml(received) {
19
+ const input = asDocument(received);
20
+ const result = await validateHtmlString(input, ['--errors-only', '--stdout']);
21
+ if (result.success) {
22
+ return {
23
+ pass: true,
24
+ message: () => 'expected HTML not to be valid, but it passed W3C validation',
25
+ };
26
+ }
27
+ return {
28
+ pass: false,
29
+ message: () => `expected HTML to be valid, but got:\n${result.stderr || result.stdout}`,
30
+ };
31
+ },
32
+ });
@@ -0,0 +1,4 @@
1
+ import { HtmlW3CValidationResult } from '../types/HtmlW3CValidator.js';
2
+ export declare function validateHtmlFile(filePath: string, options?: string[]): Promise<HtmlW3CValidationResult>;
3
+ export declare function validateHtmlString(html: string, options?: string[]): Promise<HtmlW3CValidationResult>;
4
+ //# sourceMappingURL=html-w3c-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-w3c-validator.d.ts","sourceRoot":"","sources":["../../../../src/adapters/html-w3c-validation/helpers/html-w3c-validator.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAwGpE,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAAM,EAAO,GACrB,OAAO,CAAC,uBAAuB,CAAC,CAWlC;AAUD,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,MAAM,EAAO,GACrB,OAAO,CAAC,uBAAuB,CAAC,CAUlC"}
@@ -0,0 +1,89 @@
1
+ import { spawn } from 'node:child_process';
2
+ import vnu from 'vnu-jar';
3
+ let _javaChecked = false;
4
+ async function ensureJava() {
5
+ if (_javaChecked)
6
+ return;
7
+ await new Promise((resolve, reject) => {
8
+ const child = spawn('java', ['-version'], { stdio: ['ignore', 'ignore', 'ignore'] });
9
+ child.once('error', () => reject(new Error('Java runtime not found. Install a JRE/JDK and ensure `java` is on PATH.')));
10
+ child.once('close', () => resolve());
11
+ });
12
+ _javaChecked = true;
13
+ }
14
+ function runJava(args, input, timeoutMs = 60_000) {
15
+ return new Promise((resolve, reject) => {
16
+ const child = spawn('java', args, {
17
+ shell: false,
18
+ stdio: ['pipe', 'pipe', 'pipe'],
19
+ });
20
+ let stdout = '';
21
+ let stderr = '';
22
+ let timedOut = false;
23
+ const timer = setTimeout(() => {
24
+ timedOut = true;
25
+ child.kill('SIGKILL');
26
+ }, timeoutMs);
27
+ const out = child.stdout;
28
+ const err = child.stderr;
29
+ if (out) {
30
+ out.setEncoding('utf8');
31
+ out.on('data', (chunk) => {
32
+ stdout += chunk;
33
+ });
34
+ }
35
+ if (err) {
36
+ err.setEncoding('utf8');
37
+ err.on('data', (chunk) => {
38
+ stderr += chunk;
39
+ });
40
+ }
41
+ child.on('error', (e) => {
42
+ clearTimeout(timer);
43
+ reject(e);
44
+ });
45
+ child.on('close', (code) => {
46
+ clearTimeout(timer);
47
+ if (timedOut && code !== 0) {
48
+ resolve({ code: code ?? 124, stdout, stderr: stderr || 'Validation timed out (java).' });
49
+ }
50
+ else {
51
+ resolve({ code: code ?? 0, stdout, stderr });
52
+ }
53
+ });
54
+ if (input != null && child.stdin) {
55
+ child.stdin.write(input);
56
+ child.stdin.end();
57
+ }
58
+ });
59
+ }
60
+ function buildArgs(extra) {
61
+ const base = ['--stdout', '--errors-only'];
62
+ const seen = new Set();
63
+ return [...base, ...extra].filter((flag) => {
64
+ const key = flag.toLowerCase();
65
+ const has = seen.has(key);
66
+ seen.add(key);
67
+ return !has;
68
+ });
69
+ }
70
+ export async function validateHtmlFile(filePath, options = []) {
71
+ await ensureJava();
72
+ const args = ['-jar', vnu, ...buildArgs(options), filePath];
73
+ const { code, stdout, stderr } = await runJava(args);
74
+ return {
75
+ success: code === 0,
76
+ stdout,
77
+ stderr: stderr || (code === 0 ? '' : stdout),
78
+ };
79
+ }
80
+ export async function validateHtmlString(html, options = []) {
81
+ await ensureJava();
82
+ const args = ['-jar', vnu, ...buildArgs(options), '-'];
83
+ const { code, stdout, stderr } = await runJava(args, html);
84
+ return {
85
+ success: code === 0,
86
+ stdout,
87
+ stderr: stderr || (code === 0 ? '' : stdout),
88
+ };
89
+ }
@@ -0,0 +1,6 @@
1
+ export interface HtmlW3CValidationResult {
2
+ success: boolean;
3
+ stdout: string;
4
+ stderr: string;
5
+ }
6
+ //# sourceMappingURL=HtmlW3CValidator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HtmlW3CValidator.d.ts","sourceRoot":"","sources":["../../../../src/adapters/html-w3c-validation/types/HtmlW3CValidator.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,uBAAuB;IAOtC,OAAO,EAAE,OAAO,CAAC;IAQjB,MAAM,EAAE,MAAM,CAAC;IAQf,MAAM,EAAE,MAAM,CAAC;CAChB"}
@@ -0,0 +1,5 @@
1
+ declare module 'vnu-jar' {
2
+ /** Absolute filesystem path to the bundled vnu.jar */
3
+ const vnuJarPath: string;
4
+ export = vnuJarPath; // CommonJS-style export (works with esModuleInterop)
5
+ }
@@ -2,6 +2,6 @@ import { PerfBudgetsSummary } from '../types/PerfBudget.js';
2
2
  import { ParsedAsset, ParsedBundle } from '../types/PerfBudgetAssets.js';
3
3
  export declare function escapeHtml(str: string): string;
4
4
  export declare function parseAnalyzerJson(raw: ParsedAsset[]): ParsedBundle;
5
- export declare function collectPerfBudgets(rootDir: string): PerfBudgetsSummary;
5
+ export declare function collectPerfBudgets(reportOutputDir: string): PerfBudgetsSummary;
6
6
  export declare function generatePerfBudgetsHtml(summary: PerfBudgetsSummary): string;
7
7
  //# sourceMappingURL=perfs-bundle-analysis-helper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"perfs-bundle-analysis-helper.d.ts","sourceRoot":"","sources":["../../../../src/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.ts"],"names":[],"mappings":"AAMA,OAAO,EAAoB,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAOtE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAmCD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,YAAY,CAgBlE;AASD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAmCtE;AASD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAwJ3E"}
1
+ {"version":3,"file":"perfs-bundle-analysis-helper.d.ts","sourceRoot":"","sources":["../../../../src/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.ts"],"names":[],"mappings":"AAMA,OAAO,EAAoB,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAKtE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAmCD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,YAAY,CAgBlE;AASD,wBAAgB,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,kBAAkB,CAmC9E;AASD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAwJ3E"}
@@ -3,7 +3,6 @@ import path from 'node:path';
3
3
  import { assetsBudgetsConfig } from '../../../configs/assets-budgets-config.js';
4
4
  import { evaluatePerfBudgets } from '../rules/perfs-budgets-rules.js';
5
5
  import { scoreAllPerfBudgets } from '../rules/perfs-budgets-scoring.js';
6
- const perfBudgetsReportDirName = 'perf-budgets-reports';
7
6
  export function escapeHtml(str) {
8
7
  return String(str)
9
8
  .replace(/&/g, '&amp;')
@@ -40,8 +39,8 @@ export function parseAnalyzerJson(raw) {
40
39
  assets,
41
40
  };
42
41
  }
43
- export function collectPerfBudgets(rootDir) {
44
- const reportsRoot = path.join(rootDir, perfBudgetsReportDirName);
42
+ export function collectPerfBudgets(reportOutputDir) {
43
+ const reportsRoot = path.join(reportOutputDir);
45
44
  if (!fs.existsSync(reportsRoot)) {
46
45
  throw new Error(`Reports directory not found: ${reportsRoot}`);
47
46
  }
@@ -0,0 +1,4 @@
1
+ import { CombinedTestsCoverage } from '../types/TestsCoverage.js';
2
+ export declare function collectTestsCoverage(reportRoot: string): CombinedTestsCoverage;
3
+ export declare function generateTestsCoverageHtml(summary: CombinedTestsCoverage, reportRoot: string): string;
4
+ //# sourceMappingURL=tests-coverage-analysis-helper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tests-coverage-analysis-helper.d.ts","sourceRoot":"","sources":["../../../../src/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,qBAAqB,EAKtB,MAAM,wBAAwB,CAAC;AA2HhC,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,qBAAqB,CAmC9E;AAiED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,MAAM,GACjB,MAAM,CAuER"}
@@ -0,0 +1,192 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { testsCoverageConfig } from '../../../configs/tests-coverage-config.js';
4
+ import { TESTS_COVERAGE_KEYS, } from '../types/TestsCoverage.js';
5
+ function readJsonSafe(file) {
6
+ try {
7
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ }
13
+ function readAppSummaryAt(reportRoot, app) {
14
+ return readJsonSafe(path.join(reportRoot, app, 'coverage-summary.json'));
15
+ }
16
+ function normalizePct(pct) {
17
+ const n = Number(pct);
18
+ if (!Number.isFinite(n) || n < 0)
19
+ return 0;
20
+ return +n.toFixed(2);
21
+ }
22
+ function extractWorstFilesForApp(reportRoot, app, metric = testsCoverageConfig.worstFiles.metric, threshold = testsCoverageConfig.worstFiles.threshold, count = testsCoverageConfig.worstFiles.count) {
23
+ const summary = readAppSummaryAt(reportRoot, app);
24
+ if (!summary)
25
+ return [];
26
+ const rows = [];
27
+ for (const [key, value] of Object.entries(summary)) {
28
+ if (key === 'total')
29
+ continue;
30
+ const bucket = value?.[metric];
31
+ if (!bucket)
32
+ continue;
33
+ const total = bucket.total ?? 0;
34
+ const covered = bucket.covered ?? 0;
35
+ if (!Number.isFinite(total) || total <= 0)
36
+ continue;
37
+ const percentage = normalizePct(bucket.pct);
38
+ if (percentage >= threshold)
39
+ continue;
40
+ rows.push([key, { covered, total, percentage }]);
41
+ }
42
+ rows.sort((a, b) => a[1].percentage - b[1].percentage || b[1].total - a[1].total || a[0].localeCompare(b[0]));
43
+ return rows.slice(0, Math.max(0, count));
44
+ }
45
+ function addTotals(a, b) {
46
+ const out = {};
47
+ for (const k of TESTS_COVERAGE_KEYS) {
48
+ const A = a[k] ?? { total: 0, covered: 0, pct: 0 };
49
+ const B = b[k] ?? { total: 0, covered: 0, pct: 0 };
50
+ const total = (A.total ?? 0) + (B.total ?? 0);
51
+ const covered = (A.covered ?? 0) + (B.covered ?? 0);
52
+ out[k] = {
53
+ total,
54
+ covered,
55
+ pct: total > 0 ? normalizePct((100 * covered) / total) : 0,
56
+ };
57
+ }
58
+ return out;
59
+ }
60
+ export function collectTestsCoverage(reportRoot) {
61
+ if (!fs.existsSync(reportRoot))
62
+ return { apps: {}, totals: undefined };
63
+ const appDirs = (() => {
64
+ try {
65
+ return fs
66
+ .readdirSync(reportRoot, { withFileTypes: true })
67
+ .filter((d) => d.isDirectory())
68
+ .map((d) => d.name);
69
+ }
70
+ catch {
71
+ return [];
72
+ }
73
+ })();
74
+ const apps = {};
75
+ let totals;
76
+ for (const app of appDirs) {
77
+ const summary = readAppSummaryAt(reportRoot, app);
78
+ const total = summary?.total;
79
+ if (!total)
80
+ continue;
81
+ const row = {};
82
+ for (const k of TESTS_COVERAGE_KEYS) {
83
+ const g = total[k];
84
+ row[k] = g
85
+ ? { total: g.total ?? 0, covered: g.covered ?? 0, pct: normalizePct(g.pct) }
86
+ : { total: 0, covered: 0, pct: 0 };
87
+ }
88
+ apps[app] = row;
89
+ totals = totals ? addTotals(totals, row) : row;
90
+ }
91
+ return { apps, totals };
92
+ }
93
+ function getTestsCoverageStatusColor(pct) {
94
+ const { green, orange } = testsCoverageConfig.thresholds;
95
+ if (pct >= green)
96
+ return 'green';
97
+ if (pct >= orange)
98
+ return 'orange';
99
+ return 'red';
100
+ }
101
+ function renderWorstFiles(app, worst) {
102
+ const { threshold, metric } = testsCoverageConfig.worstFiles;
103
+ if (!worst.length) {
104
+ return `
105
+ <details>
106
+ <summary>⚠️ Worst covered files (${metric})</summary>
107
+ <p style="margin:0.5rem 0; color:#888;">No files below threshold 🎉</p>
108
+ </details>`;
109
+ }
110
+ const rows = worst
111
+ .map(([file, f]) => `
112
+ <tr>
113
+ <td colspan="2">${file}</td>
114
+ <td>${f.covered}/${f.total}</td>
115
+ <td style="color:${getTestsCoverageStatusColor(f.percentage)}">${f.percentage.toFixed(2)}%</td>
116
+ </tr>`)
117
+ .join('');
118
+ return `
119
+ <details>
120
+ <summary>⚠️ Worst covered files (below ${threshold}% — ${metric})</summary>
121
+ <table style="margin:0.5rem 0; border:1px solid #ccc; width:95%;">
122
+ <tbody>${rows}</tbody>
123
+ </table>
124
+ </details>`;
125
+ }
126
+ export function generateTestsCoverageHtml(summary, reportRoot) {
127
+ const head = `
128
+ <style>
129
+ body { font-family: Arial, sans-serif; margin: 2rem; }
130
+ h1 { margin-bottom: 1rem; }
131
+ table { border-collapse: collapse; width: 100%; margin-bottom: 2rem; }
132
+ th, td { border: 1px solid #ccc; padding: 0.5rem; text-align: left; vertical-align: top; }
133
+ th { background: #f0f0f0; }
134
+ details summary { cursor: pointer; font-weight: bold; }
135
+ .green { color: #0a0; }
136
+ .orange { color: #c80; }
137
+ .red { color: #c00; }
138
+ .row-green { background:#e6ffed; }
139
+ .row-orange { background:#fff8e1; }
140
+ .row-red { background:#ffe6e6; }
141
+ </style>
142
+ <h1>Tests Coverage</h1>
143
+ `;
144
+ const appRow = (app, r) => {
145
+ const status = getTestsCoverageStatusColor(r.lines?.pct ?? 0);
146
+ return `
147
+ <tr class="row-${status}">
148
+ <td>${app}</td>
149
+ ${TESTS_COVERAGE_KEYS.map((k) => {
150
+ const pct = r[k]?.pct ?? 0;
151
+ const cls = getTestsCoverageStatusColor(pct);
152
+ return `<td class="${cls}" style="font-weight:600">${pct}%</td>`;
153
+ }).join('')}
154
+ </tr>
155
+ <tr>
156
+ <td colspan="5">
157
+ ${renderWorstFiles(app, extractWorstFilesForApp(reportRoot, app))}
158
+ </td>
159
+ </tr>
160
+ `;
161
+ };
162
+ const totalsRow = summary.totals
163
+ ? `
164
+ <tr>
165
+ <td><b>Totals</b></td>
166
+ ${TESTS_COVERAGE_KEYS.map((k) => {
167
+ const pct = summary.totals?.[k]?.pct ?? 0;
168
+ const cls = getTestsCoverageStatusColor(pct);
169
+ return `<td class="${cls}"><b>${pct}%</b></td>`;
170
+ }).join('')}
171
+ </tr>`
172
+ : '';
173
+ const table = `
174
+ <table>
175
+ <thead>
176
+ <tr>
177
+ <th>Application</th>
178
+ <th>Lines %</th>
179
+ <th>Branches %</th>
180
+ <th>Functions %</th>
181
+ <th>Statements %</th>
182
+ </tr>
183
+ </thead>
184
+ <tbody>
185
+ ${Object.entries(summary.apps)
186
+ .map(([app, r]) => appRow(app, r))
187
+ .join('')}
188
+ ${totalsRow}
189
+ </tbody>
190
+ </table>`;
191
+ return `<!doctype html><meta charset="utf-8">${head}${table}`;
192
+ }
@@ -0,0 +1,36 @@
1
+ export type TestsCoverageKey = 'lines' | 'branches' | 'functions' | 'statements';
2
+ export declare const TESTS_COVERAGE_KEYS: readonly TestsCoverageKey[];
3
+ export interface TestsCoverageBucket {
4
+ total: number;
5
+ covered: number;
6
+ pct: number;
7
+ }
8
+ export type TestsCoverageRow = Record<TestsCoverageKey, TestsCoverageBucket>;
9
+ export interface CombinedTestsCoverage {
10
+ apps: Record<string, TestsCoverageRow>;
11
+ totals?: TestsCoverageRow;
12
+ }
13
+ export interface TestsCoverageSummary {
14
+ total?: Partial<Record<TestsCoverageKey, Partial<TestsCoverageBucket>>>;
15
+ }
16
+ export type TestsCoverageMetric = keyof TestsCoverageRow;
17
+ export interface WorstFilesOptions {
18
+ reportRoot: string;
19
+ count: number;
20
+ metric: TestsCoverageMetric;
21
+ }
22
+ export interface FileCoverageRow {
23
+ file: string;
24
+ total: number;
25
+ covered: number;
26
+ pct: number;
27
+ }
28
+ export type WorstFileEntry = readonly [
29
+ file: string,
30
+ stats: {
31
+ covered: number;
32
+ total: number;
33
+ percentage: number;
34
+ }
35
+ ];
36
+ //# sourceMappingURL=TestsCoverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestsCoverage.d.ts","sourceRoot":"","sources":["../../../../src/adapters/tests-coverage/types/TestsCoverage.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,UAAU,GAAG,WAAW,GAAG,YAAY,CAAC;AAOjF,eAAO,MAAM,mBAAmB,EAAE,SAAS,gBAAgB,EAKjD,CAAC;AAOX,MAAM,WAAW,mBAAmB;IAElC,KAAK,EAAE,MAAM,CAAC;IAEd,OAAO,EAAE,MAAM,CAAC;IAEhB,GAAG,EAAE,MAAM,CAAC;CACb;AAOD,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,CAAC;AAQ7E,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AAmBD,MAAM,WAAW,oBAAoB;IAEnC,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;CACzE;AAMD,MAAM,MAAM,mBAAmB,GAAG,MAAM,gBAAgB,CAAC;AAKzD,MAAM,WAAW,iBAAiB;IAEhC,UAAU,EAAE,MAAM,CAAC;IAEnB,KAAK,EAAE,MAAM,CAAC;IAEd,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AAOD,MAAM,WAAW,eAAe;IAE9B,IAAI,EAAE,MAAM,CAAC;IAEb,KAAK,EAAE,MAAM,CAAC;IAEd,OAAO,EAAE,MAAM,CAAC;IAEhB,GAAG,EAAE,MAAM,CAAC;CACb;AAQD,MAAM,MAAM,cAAc,GAAG,SAAS;IACpC,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE;CAC9D,CAAC"}
@@ -0,0 +1,6 @@
1
+ export const TESTS_COVERAGE_KEYS = [
2
+ 'lines',
3
+ 'branches',
4
+ 'functions',
5
+ 'statements',
6
+ ];
@@ -0,0 +1,4 @@
1
+ import { CoverageSummary } from '../types/TypesCoverage.js';
2
+ export declare function collectTypesCoverage(reportOutputDir: string): CoverageSummary;
3
+ export declare function generateTypesCoverageHtml(summary: CoverageSummary): string;
4
+ //# sourceMappingURL=types-coverage-analysis-helper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-coverage-analysis-helper.d.ts","sourceRoot":"","sources":["../../../../src/adapters/types-coverage/helpers/types-coverage-analysis-helper.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,eAAe,EAGhB,MAAM,wBAAwB,CAAC;AAoChC,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,MAAM,GAAG,eAAe,CAkD7E;AA6ED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAgC1E"}
@@ -0,0 +1,158 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { typesCoverageConfig } from '../../../configs/types-coverage-config.js';
4
+ function normalizeFileEntry(file, data) {
5
+ const covered = data.correctCount ?? 0;
6
+ const total = data.totalCount ?? 0;
7
+ return {
8
+ file,
9
+ covered,
10
+ total,
11
+ percentage: total > 0 ? (covered / total) * 100 : 0,
12
+ };
13
+ }
14
+ function extractWorstFiles(report) {
15
+ if (!report.fileCounts)
16
+ return [];
17
+ const { threshold, count } = typesCoverageConfig.worstFiles;
18
+ return Object.entries(report.fileCounts)
19
+ .map(([file, f]) => normalizeFileEntry(file, f))
20
+ .filter((f) => f.total > 0 && f.percentage < threshold)
21
+ .sort((a, b) => a.percentage - b.percentage)
22
+ .slice(0, count)
23
+ .map((f) => [f.file, { covered: f.covered, total: f.total, percentage: f.percentage }]);
24
+ }
25
+ export function collectTypesCoverage(reportOutputDir) {
26
+ const apps = {};
27
+ let totalCovered = 0;
28
+ let totalTypes = 0;
29
+ if (!fs.existsSync(reportOutputDir)) {
30
+ return { apps: {}, totalCovered: 0, totalTypes: 0, percentage: 0 };
31
+ }
32
+ const appDirs = fs
33
+ .readdirSync(reportOutputDir)
34
+ .filter((entry) => fs.statSync(path.join(reportOutputDir, entry)).isDirectory());
35
+ for (const appDirName of appDirs) {
36
+ const jsonPath = path.join(reportOutputDir, appDirName, 'typescript-coverage.json');
37
+ if (!fs.existsSync(jsonPath))
38
+ continue;
39
+ try {
40
+ const report = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
41
+ const covered = report.covered ?? 0;
42
+ const total = report.total ?? 0;
43
+ const percentage = total > 0 ? (covered / total) * 100 : 0;
44
+ const appShortName = appDirName.replace(/^manager-/, '').replace(/-app$/, '');
45
+ apps[appShortName] = {
46
+ covered,
47
+ total,
48
+ percentage,
49
+ worstFiles: extractWorstFiles(report),
50
+ };
51
+ totalCovered += covered;
52
+ totalTypes += total;
53
+ }
54
+ catch (err) {
55
+ apps[appDirName] = {
56
+ covered: 0,
57
+ total: 0,
58
+ percentage: 0,
59
+ error: err.message,
60
+ };
61
+ }
62
+ }
63
+ return {
64
+ apps,
65
+ totalCovered,
66
+ totalTypes,
67
+ percentage: totalTypes > 0 ? (totalCovered / totalTypes) * 100 : 0,
68
+ };
69
+ }
70
+ function getReportStatusColor(percentage) {
71
+ const { green, orange } = typesCoverageConfig.thresholds;
72
+ if (percentage >= green)
73
+ return 'green';
74
+ if (percentage >= orange)
75
+ return 'orange';
76
+ return 'red';
77
+ }
78
+ function renderWorstFiles(app, worstFiles = []) {
79
+ if (!worstFiles.length) {
80
+ return `
81
+ <details>
82
+ <summary>⚠️ Worst covered files</summary>
83
+ <p style="margin:0.5rem 0; color:#888;">No files below threshold 🎉</p>
84
+ </details>`;
85
+ }
86
+ const rows = worstFiles
87
+ .map(([file, f]) => `
88
+ <tr>
89
+ <td colspan="2">${file}</td>
90
+ <td>${f.covered}/${f.total}</td>
91
+ <td style="color:${getReportStatusColor(f.percentage)}">
92
+ ${f.percentage.toFixed(2)}%
93
+ </td>
94
+ </tr>`)
95
+ .join('');
96
+ return `
97
+ <details>
98
+ <summary>⚠️ Worst covered files (below ${typesCoverageConfig.worstFiles.threshold}%)</summary>
99
+ <table style="margin:0.5rem 0; border:1px solid #ccc; width:95%;">
100
+ <tbody>${rows}</tbody>
101
+ </table>
102
+ </details>`;
103
+ }
104
+ function buildRows(apps) {
105
+ return Object.entries(apps)
106
+ .map(([app, stats]) => {
107
+ if (stats.error) {
108
+ return `<tr style="background:#ffe0e0">
109
+ <td>${app}</td>
110
+ <td colspan="3" style="color:red">${stats.error}</td>
111
+ </tr>`;
112
+ }
113
+ const status = getReportStatusColor(stats.percentage);
114
+ const statusBg = status === 'green' ? '#e6ffed' : status === 'orange' ? '#fff8e1' : '#ffe6e6';
115
+ return `
116
+ <tr style="background:${statusBg}">
117
+ <td>${app}</td>
118
+ <td>${stats.covered}</td>
119
+ <td>${stats.total}</td>
120
+ <td style="font-weight:bold;">${stats.percentage.toFixed(2)}%</td>
121
+ </tr>
122
+ <tr><td colspan="4">${renderWorstFiles(app, stats.worstFiles)}</td></tr>`;
123
+ })
124
+ .join('');
125
+ }
126
+ export function generateTypesCoverageHtml(summary) {
127
+ return `<!DOCTYPE html>
128
+ <html lang="en">
129
+ <head>
130
+ <meta charset="UTF-8"/>
131
+ <title>TypeScript Types Coverage Report</title>
132
+ <style>
133
+ body { font-family: Arial, sans-serif; margin: 2rem; }
134
+ h1 { margin-bottom: 1rem; }
135
+ table { border-collapse: collapse; width: 100%; margin-bottom: 2rem; }
136
+ th, td { border: 1px solid #ccc; padding: 0.5rem; text-align: left; vertical-align: top; }
137
+ th { background: #f0f0f0; }
138
+ details summary { cursor: pointer; font-weight: bold; }
139
+ </style>
140
+ </head>
141
+ <body>
142
+ <h1>TypeScript Types Coverage Report</h1>
143
+ <p>Total: ${summary.totalCovered} / ${summary.totalTypes}
144
+ (${summary.percentage.toFixed(2)}%)</p>
145
+ <table>
146
+ <thead>
147
+ <tr>
148
+ <th>App</th>
149
+ <th>Covered</th>
150
+ <th>Total</th>
151
+ <th>Coverage</th>
152
+ </tr>
153
+ </thead>
154
+ <tbody>${buildRows(summary.apps)}</tbody>
155
+ </table>
156
+ </body>
157
+ </html>`;
158
+ }
@@ -0,0 +1,30 @@
1
+ export interface RawCoverageReport {
2
+ percentage?: number;
3
+ total?: number;
4
+ covered?: number;
5
+ uncovered?: number;
6
+ fileCounts?: Record<string, {
7
+ correctCount: number;
8
+ totalCount: number;
9
+ }>;
10
+ }
11
+ export interface FileCoverage {
12
+ covered: number;
13
+ total: number;
14
+ percentage: number;
15
+ }
16
+ export type WorstFileEntry = [string, FileCoverage];
17
+ export interface AppCoverageSummary {
18
+ covered: number;
19
+ total: number;
20
+ percentage: number;
21
+ error?: string;
22
+ worstFiles?: WorstFileEntry[];
23
+ }
24
+ export interface CoverageSummary {
25
+ apps: Record<string, AppCoverageSummary>;
26
+ totalCovered: number;
27
+ totalTypes: number;
28
+ percentage: number;
29
+ }
30
+ //# sourceMappingURL=TypesCoverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TypesCoverage.d.ts","sourceRoot":"","sources":["../../../../src/adapters/types-coverage/types/TypesCoverage.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CACjB,MAAM,EACN;QACE,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;KACpB,CACF,CAAC;CACH;AAKD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAGD,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AAKpD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;CAC/B;AAKD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB"}