@ovh-ux/manager-static-analysis-kit 0.8.0 → 0.10.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.
- package/CHANGELOG.md +37 -0
- package/README.md +59 -69
- package/bin/cli-path-config.js +31 -0
- package/bin/code-duplication-cli.js +96 -0
- package/bin/code-duplication-tests-cli.js +68 -0
- package/bin/lint-cli.js +1 -1
- package/bin/perf-budgets-cli.js +58 -297
- package/bin/perf-budgets-tests-cli.js +1 -3
- package/bin/static-dynamic-quality-checks.js +138 -0
- package/bin/tests-coverage-cli-tests.js +67 -0
- package/bin/tests-coverage-cli.js +137 -0
- package/bin/types-coverage-cli.js +195 -0
- package/bin/types-coverage-tests-cli.js +68 -0
- package/bin/utils/apps-utils.js +64 -0
- package/bin/utils/args-parse-utils.js +229 -0
- package/bin/utils/file-utils.js +52 -0
- package/bin/utils/log-utils.js +29 -0
- package/bin/utils/runner-utils.js +85 -0
- package/bin/utils/turbo-utils.js +150 -0
- package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.d.ts +9 -0
- package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.d.ts.map +1 -0
- package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.js +294 -0
- package/dist/adapters/code-duplication/types/CodeDuplication.d.ts +63 -0
- package/dist/adapters/code-duplication/types/CodeDuplication.d.ts.map +1 -0
- package/dist/adapters/code-duplication/types/CodeDuplication.js +1 -0
- package/dist/adapters/eslint/rules/eslint-prettier.js +1 -1
- package/dist/adapters/html-a11y-validation/helpers/html-a11y-tests-setup.d.ts +62 -0
- package/dist/adapters/html-a11y-validation/helpers/html-a11y-tests-setup.d.ts.map +1 -0
- package/dist/adapters/html-a11y-validation/helpers/html-a11y-tests-setup.js +73 -0
- package/dist/adapters/html-w3c-validation/helpers/html-w3c-tests-setup.d.ts +19 -0
- package/dist/adapters/html-w3c-validation/helpers/html-w3c-tests-setup.d.ts.map +1 -0
- package/dist/adapters/html-w3c-validation/helpers/html-w3c-tests-setup.js +32 -0
- package/dist/adapters/html-w3c-validation/helpers/html-w3c-validator.d.ts +4 -0
- package/dist/adapters/html-w3c-validation/helpers/html-w3c-validator.d.ts.map +1 -0
- package/dist/adapters/html-w3c-validation/helpers/html-w3c-validator.js +89 -0
- package/dist/adapters/html-w3c-validation/types/HtmlW3CValidator.d.ts +6 -0
- package/dist/adapters/html-w3c-validation/types/HtmlW3CValidator.d.ts.map +1 -0
- package/dist/adapters/html-w3c-validation/types/HtmlW3CValidator.js +1 -0
- package/dist/adapters/html-w3c-validation/types/globals/vnu-jar.d.ts +5 -0
- package/dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.d.ts +1 -1
- package/dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.d.ts.map +1 -1
- package/dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.js +2 -3
- package/dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.d.ts +4 -0
- package/dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.d.ts.map +1 -0
- package/dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.js +215 -0
- package/dist/adapters/tests-coverage/types/TestsCoverage.d.ts +36 -0
- package/dist/adapters/tests-coverage/types/TestsCoverage.d.ts.map +1 -0
- package/dist/adapters/tests-coverage/types/TestsCoverage.js +6 -0
- package/dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.d.ts +4 -0
- package/dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.d.ts.map +1 -0
- package/dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.js +231 -0
- package/dist/adapters/types-coverage/types/TypesCoverage.d.ts +38 -0
- package/dist/adapters/types-coverage/types/TypesCoverage.d.ts.map +1 -0
- package/dist/adapters/types-coverage/types/TypesCoverage.js +1 -0
- package/dist/configs/architecture-analysis-config.d.ts +5 -0
- package/dist/configs/architecture-analysis-config.d.ts.map +1 -0
- package/dist/configs/{madge-config.js → architecture-analysis-config.js} +1 -1
- package/dist/configs/code-duplication-config.d.ts +22 -0
- package/dist/configs/code-duplication-config.d.ts.map +1 -0
- package/dist/configs/code-duplication-config.js +22 -0
- package/dist/configs/{prettier-config.d.ts → code-formatter-config.d.ts} +1 -1
- package/dist/configs/code-formatter-config.d.ts.map +1 -0
- package/dist/configs/file-globs-config.d.ts +0 -3
- package/dist/configs/file-globs-config.d.ts.map +1 -1
- package/dist/configs/file-globs-config.js +0 -3
- package/dist/configs/naming-conventions-config.d.ts.map +1 -1
- package/dist/configs/naming-conventions-config.js +2 -0
- package/dist/configs/shared-exclusion-patterns.d.ts +1 -0
- package/dist/configs/shared-exclusion-patterns.d.ts.map +1 -1
- package/dist/configs/shared-exclusion-patterns.js +23 -1
- package/dist/configs/tests-coverage-config.d.ts +12 -0
- package/dist/configs/tests-coverage-config.d.ts.map +1 -0
- package/dist/configs/tests-coverage-config.js +11 -0
- package/dist/configs/types-coverage-config.d.ts +33 -0
- package/dist/configs/types-coverage-config.d.ts.map +1 -0
- package/dist/configs/types-coverage-config.js +24 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +32 -7
- package/bin/cli-utils.js +0 -56
- package/dist/configs/jscpd-config.d.ts +0 -7
- package/dist/configs/jscpd-config.d.ts.map +0 -1
- package/dist/configs/jscpd-config.js +0 -7
- package/dist/configs/madge-config.d.ts +0 -5
- package/dist/configs/madge-config.d.ts.map +0 -1
- package/dist/configs/prettier-config.d.ts.map +0 -1
- /package/dist/configs/{prettier-config.js → code-formatter-config.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import prettier from 'eslint-plugin-prettier';
|
|
2
|
+
import { prettierConfig, prettierIgnorePatterns } from '../../../configs/code-formatter-config.js';
|
|
2
3
|
import { commonTextFiles } from '../../../configs/file-globs-config.js';
|
|
3
|
-
import { prettierConfig, prettierIgnorePatterns } from '../../../configs/prettier-config.js';
|
|
4
4
|
export const prettierEslintConfig = {
|
|
5
5
|
files: [commonTextFiles],
|
|
6
6
|
ignores: prettierIgnorePatterns,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* Ambient module augmentation for the custom Vitest matcher `toBeAccessible`.
|
|
4
|
+
* Import the setup file
|
|
5
|
+
* `@ovh-ux/manager-static-analysis-kit/tests/html-a11y-tests-setup`
|
|
6
|
+
* in your test setup to register the matcher at runtime and include these types.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import 'vitest';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options object accepted by the accessibility matcher.
|
|
13
|
+
* This is intentionally generic so the underlying engine can evolve
|
|
14
|
+
* without a breaking public API. Keys/values are implementation-defined.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* await expect(container).toBeAccessible({
|
|
18
|
+
* rules: { 'color-contrast': { enabled: false } },
|
|
19
|
+
* runOnly: ['wcag2a', 'wcag2aa'],
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
export interface HtmlA11yOptions {
|
|
23
|
+
/** Arbitrary engine-specific options (e.g., rules, tags, runOnly, selectors). */
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare module 'vitest' {
|
|
28
|
+
interface Assertion<T = any> {
|
|
29
|
+
/**
|
|
30
|
+
* Assert that a DOM container (Element/Document/DocumentFragment)
|
|
31
|
+
* has no accessibility violations, as determined by the toolkit’s
|
|
32
|
+
* internal engine.
|
|
33
|
+
*
|
|
34
|
+
* The matcher is registered by importing:
|
|
35
|
+
* `@ovh-ux/manager-static-analysis-kit/tests/html-a11y-tests-setup`
|
|
36
|
+
* in your test setup file.
|
|
37
|
+
*
|
|
38
|
+
* @param options Optional engine-specific configuration (see {@link HtmlA11yOptions}).
|
|
39
|
+
* @returns A promise that resolves when the assertion completes.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const { container } = render(<Button />);
|
|
43
|
+
* await expect(container).toBeAccessible();
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* await expect(container).toBeAccessible({
|
|
47
|
+
* rules: { 'color-contrast': { enabled: false } },
|
|
48
|
+
* });
|
|
49
|
+
*/
|
|
50
|
+
toBeAccessible(options?: HtmlA11yOptions): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface AsymmetricMatchersContaining {
|
|
54
|
+
/**
|
|
55
|
+
* Asymmetric form of {@link Assertion.toBeAccessible}.
|
|
56
|
+
* Useful with `.not` and partial matching expressions.
|
|
57
|
+
*
|
|
58
|
+
* @param options Optional engine-specific configuration.
|
|
59
|
+
*/
|
|
60
|
+
toBeAccessible(options?: HtmlA11yOptions): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-a11y-tests-setup.d.ts","sourceRoot":"","sources":["../../../../src/adapters/html-a11y-validation/helpers/html-a11y-tests-setup.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import axe from 'axe-core';
|
|
2
|
+
import { expect } from 'vitest';
|
|
3
|
+
const NODE = { ELEMENT: 1, DOCUMENT: 9, FRAGMENT: 11 };
|
|
4
|
+
function assertSupportedDomEnv() {
|
|
5
|
+
const ua = typeof navigator !== 'undefined' && navigator.userAgent
|
|
6
|
+
? navigator.userAgent.toLowerCase()
|
|
7
|
+
: '';
|
|
8
|
+
if (ua.includes('happy-dom')) {
|
|
9
|
+
throw new Error('[a11y] The test DOM environment is not supported. Please use the "jsdom" environment.');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function isDomNode(x) {
|
|
13
|
+
return typeof x === 'object' && x !== null && 'nodeType' in x;
|
|
14
|
+
}
|
|
15
|
+
function isElement(x) {
|
|
16
|
+
return isDomNode(x) && x.nodeType === NODE.ELEMENT;
|
|
17
|
+
}
|
|
18
|
+
function isDocument(x) {
|
|
19
|
+
return isDomNode(x) && x.nodeType === NODE.DOCUMENT;
|
|
20
|
+
}
|
|
21
|
+
function isFragment(x) {
|
|
22
|
+
return isDomNode(x) && x.nodeType === NODE.FRAGMENT;
|
|
23
|
+
}
|
|
24
|
+
function toAxeContext(target) {
|
|
25
|
+
if (isElement(target) || isDocument(target)) {
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
28
|
+
if (isFragment(target)) {
|
|
29
|
+
const elements = Array.from(target.childNodes).filter((n) => n.nodeType === NODE.ELEMENT);
|
|
30
|
+
if (elements.length > 0) {
|
|
31
|
+
return { include: elements };
|
|
32
|
+
}
|
|
33
|
+
return { include: [target] };
|
|
34
|
+
}
|
|
35
|
+
return target;
|
|
36
|
+
}
|
|
37
|
+
async function runA11y(target, options) {
|
|
38
|
+
const context = toAxeContext(target);
|
|
39
|
+
const opts = options ?? {};
|
|
40
|
+
return axe.run(context, opts);
|
|
41
|
+
}
|
|
42
|
+
function formatViolations(results) {
|
|
43
|
+
const { violations } = results;
|
|
44
|
+
if (!violations.length)
|
|
45
|
+
return '';
|
|
46
|
+
return violations
|
|
47
|
+
.map((v) => {
|
|
48
|
+
const where = v.nodes
|
|
49
|
+
.map((n) => ` - ${n.target.join(' ')}${n.failureSummary ? `\n ${n.failureSummary}` : ''}`)
|
|
50
|
+
.join('\n');
|
|
51
|
+
return `[${v.id}] ${v.help}\n${where}\nMore: ${v.helpUrl}`;
|
|
52
|
+
})
|
|
53
|
+
.join('\n\n');
|
|
54
|
+
}
|
|
55
|
+
expect.extend({
|
|
56
|
+
async toBeAccessible(received, options) {
|
|
57
|
+
assertSupportedDomEnv();
|
|
58
|
+
if (!isDomNode(received)) {
|
|
59
|
+
return {
|
|
60
|
+
pass: false,
|
|
61
|
+
message: () => 'toBeAccessible(): expected an Element/Document/DocumentFragment (e.g., RTL container).',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const results = await runA11y(received, options);
|
|
65
|
+
const pass = results.violations.length === 0;
|
|
66
|
+
return {
|
|
67
|
+
pass,
|
|
68
|
+
message: () => pass
|
|
69
|
+
? 'expected container NOT to be accessible, but no violations were found'
|
|
70
|
+
: `Accessibility violations found (${results.violations.length}):\n\n${formatViolations(results)}`,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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(
|
|
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;
|
|
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, '&')
|
|
@@ -40,8 +39,8 @@ export function parseAnalyzerJson(raw) {
|
|
|
40
39
|
assets,
|
|
41
40
|
};
|
|
42
41
|
}
|
|
43
|
-
export function collectPerfBudgets(
|
|
44
|
-
const reportsRoot = path.join(
|
|
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,CA6FR"}
|
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
if (!worst.length) {
|
|
103
|
+
return `
|
|
104
|
+
<details class="coverage-details">
|
|
105
|
+
<summary>📊 Test Coverage Overview for <strong>${app}</strong></summary>
|
|
106
|
+
<p class="coverage-empty">No files below threshold 🎉</p>
|
|
107
|
+
</details>`;
|
|
108
|
+
}
|
|
109
|
+
const rows = worst
|
|
110
|
+
.map(([file, f]) => `
|
|
111
|
+
<tr>
|
|
112
|
+
<td colspan="2">${file}</td>
|
|
113
|
+
<td>${f.covered}/${f.total}</td>
|
|
114
|
+
<td style="color:${getTestsCoverageStatusColor(f.percentage)}">
|
|
115
|
+
${f.percentage.toFixed(2)}%
|
|
116
|
+
</td>
|
|
117
|
+
</tr>`)
|
|
118
|
+
.join('');
|
|
119
|
+
return `
|
|
120
|
+
<details class="coverage-details">
|
|
121
|
+
<summary>📊 Test Coverage Overview for <strong>${app}</strong></summary>
|
|
122
|
+
<table class="coverage-subtable">
|
|
123
|
+
<tbody>${rows}</tbody>
|
|
124
|
+
</table>
|
|
125
|
+
</details>`;
|
|
126
|
+
}
|
|
127
|
+
export function generateTestsCoverageHtml(summary, reportRoot) {
|
|
128
|
+
const head = `
|
|
129
|
+
<style>
|
|
130
|
+
body { font-family: Arial, sans-serif; margin: 2rem; }
|
|
131
|
+
h1 { margin-bottom: 1rem; }
|
|
132
|
+
table { border-collapse: collapse; width: 100%; margin-bottom: 2rem; }
|
|
133
|
+
th, td { border: 1px solid #ccc; padding: 0.5rem; text-align: left; vertical-align: top; }
|
|
134
|
+
th { background: #f0f0f0; }
|
|
135
|
+
details summary { cursor: pointer; font-weight: bold; }
|
|
136
|
+
.green { color: #0a0; }
|
|
137
|
+
.orange { color: #c80; }
|
|
138
|
+
.red { color: #c00; }
|
|
139
|
+
.row-green { background:#e6ffed; }
|
|
140
|
+
.row-orange { background:#fff8e1; }
|
|
141
|
+
.row-red { background:#ffe6e6; }
|
|
142
|
+
.coverage-details {
|
|
143
|
+
margin: 0.3rem 0 0.8rem 1.5rem; /* indent */
|
|
144
|
+
padding: 0.5rem;
|
|
145
|
+
border-left: 3px solid #ccc;
|
|
146
|
+
background: #fafafa;
|
|
147
|
+
border-radius: 4px;
|
|
148
|
+
}
|
|
149
|
+
.coverage-details summary {
|
|
150
|
+
font-weight: bold;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
}
|
|
153
|
+
.coverage-subtable {
|
|
154
|
+
margin-top: 0.5rem;
|
|
155
|
+
border: 1px solid #ddd;
|
|
156
|
+
width: 95%;
|
|
157
|
+
font-size: 0.9em;
|
|
158
|
+
}
|
|
159
|
+
.coverage-empty {
|
|
160
|
+
margin: 0.5rem 0;
|
|
161
|
+
color: #888;
|
|
162
|
+
font-style: italic;
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
165
|
+
<h1>Tests Coverage</h1>
|
|
166
|
+
`;
|
|
167
|
+
const appRow = (app, r) => {
|
|
168
|
+
const status = getTestsCoverageStatusColor(r.lines?.pct ?? 0);
|
|
169
|
+
return `
|
|
170
|
+
<tr class="row-${status}">
|
|
171
|
+
<td>${app}</td>
|
|
172
|
+
${TESTS_COVERAGE_KEYS.map((k) => {
|
|
173
|
+
const pct = r[k]?.pct ?? 0;
|
|
174
|
+
const cls = getTestsCoverageStatusColor(pct);
|
|
175
|
+
return `<td class="${cls}" style="font-weight:600">${pct}%</td>`;
|
|
176
|
+
}).join('')}
|
|
177
|
+
</tr>
|
|
178
|
+
<tr>
|
|
179
|
+
<td colspan="5">
|
|
180
|
+
${renderWorstFiles(app, extractWorstFilesForApp(reportRoot, app))}
|
|
181
|
+
</td>
|
|
182
|
+
</tr>
|
|
183
|
+
`;
|
|
184
|
+
};
|
|
185
|
+
const totalsRow = summary.totals
|
|
186
|
+
? `
|
|
187
|
+
<tr>
|
|
188
|
+
<td><b>Totals</b></td>
|
|
189
|
+
${TESTS_COVERAGE_KEYS.map((k) => {
|
|
190
|
+
const pct = summary.totals?.[k]?.pct ?? 0;
|
|
191
|
+
const cls = getTestsCoverageStatusColor(pct);
|
|
192
|
+
return `<td class="${cls}"><b>${pct}%</b></td>`;
|
|
193
|
+
}).join('')}
|
|
194
|
+
</tr>`
|
|
195
|
+
: '';
|
|
196
|
+
const table = `
|
|
197
|
+
<table>
|
|
198
|
+
<thead>
|
|
199
|
+
<tr>
|
|
200
|
+
<th>Application</th>
|
|
201
|
+
<th>Lines %</th>
|
|
202
|
+
<th>Branches %</th>
|
|
203
|
+
<th>Functions %</th>
|
|
204
|
+
<th>Statements %</th>
|
|
205
|
+
</tr>
|
|
206
|
+
</thead>
|
|
207
|
+
<tbody>
|
|
208
|
+
${Object.entries(summary.apps)
|
|
209
|
+
.map(([app, r]) => appRow(app, r))
|
|
210
|
+
.join('')}
|
|
211
|
+
${totalsRow}
|
|
212
|
+
</tbody>
|
|
213
|
+
</table>`;
|
|
214
|
+
return `<!doctype html><meta charset="utf-8">${head}${table}`;
|
|
215
|
+
}
|
|
@@ -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,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,EAIhB,MAAM,wBAAwB,CAAC;AA2EhC,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,MAAM,GAAG,eAAe,CA4C7E;AA+HD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAwD1E"}
|