@ovh-ux/manager-static-analysis-kit 0.7.1 → 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.
- package/CHANGELOG.md +48 -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 +18 -16
- package/bin/perf-budgets-cli.js +120 -0
- package/bin/perf-budgets-tests-cli.js +72 -0
- package/bin/static-dynamic-quality-checks.js +138 -0
- package/bin/tests-coverage-cli-tests.js +67 -0
- package/bin/tests-coverage-cli.js +135 -0
- package/bin/types-coverage-cli.js +127 -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 +222 -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 +139 -0
- package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.d.ts +6 -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 +240 -0
- package/dist/adapters/code-duplication/types/CodeDuplication.d.ts +60 -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-naming-conventions.d.ts.map +1 -1
- package/dist/adapters/eslint/rules/eslint-naming-conventions.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 +7 -0
- package/dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.d.ts.map +1 -0
- package/dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.js +191 -0
- package/dist/adapters/perf-budgets/rules/perfs-budgets-rules.d.ts +10 -0
- package/dist/adapters/perf-budgets/rules/perfs-budgets-rules.d.ts.map +1 -0
- package/dist/adapters/perf-budgets/rules/perfs-budgets-rules.js +20 -0
- package/dist/adapters/perf-budgets/rules/perfs-budgets-scoring.d.ts +5 -0
- package/dist/adapters/perf-budgets/rules/perfs-budgets-scoring.d.ts.map +1 -0
- package/dist/adapters/perf-budgets/rules/perfs-budgets-scoring.js +35 -0
- package/dist/adapters/perf-budgets/types/PerfBudget.d.ts +22 -0
- package/dist/adapters/perf-budgets/types/PerfBudget.d.ts.map +1 -0
- package/dist/adapters/perf-budgets/types/PerfBudget.js +1 -0
- package/dist/adapters/perf-budgets/types/PerfBudgetAssets.d.ts +13 -0
- package/dist/adapters/perf-budgets/types/PerfBudgetAssets.d.ts.map +1 -0
- package/dist/adapters/perf-budgets/types/PerfBudgetAssets.js +1 -0
- package/dist/adapters/perf-budgets/types/PerfBudgetScore.d.ts +13 -0
- package/dist/adapters/perf-budgets/types/PerfBudgetScore.d.ts.map +1 -0
- package/dist/adapters/perf-budgets/types/PerfBudgetScore.js +1 -0
- 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 +192 -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 +158 -0
- package/dist/adapters/types-coverage/types/TypesCoverage.d.ts +30 -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/assets-budgets-config.d.ts +28 -0
- package/dist/configs/assets-budgets-config.d.ts.map +1 -0
- package/dist/configs/assets-budgets-config.js +45 -0
- package/dist/configs/bundle-analysis-config.d.ts +8 -0
- package/dist/configs/bundle-analysis-config.d.ts.map +1 -0
- package/dist/configs/bundle-analysis-config.js +7 -0
- package/dist/configs/code-duplication-config.d.ts +21 -0
- package/dist/configs/code-duplication-config.d.ts.map +1 -0
- package/dist/configs/code-duplication-config.js +21 -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 +1 -4
- package/dist/configs/file-globs-config.d.ts.map +1 -1
- package/dist/configs/file-globs-config.js +1 -4
- 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.map +1 -1
- package/dist/configs/shared-exclusion-patterns.js +3 -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 +19 -0
- package/dist/configs/types-coverage-config.d.ts.map +1 -0
- package/dist/configs/types-coverage-config.js +27 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +35 -7
- 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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
import { codeDuplicationConfig } from '../../dist/configs/code-duplication-config.js';
|
|
6
|
+
import { typesCoverageConfig } from '../../dist/configs/types-coverage-config.js';
|
|
7
|
+
import { isReactApp } from './apps-utils.js';
|
|
8
|
+
import { logError, logInfo, logWarn } from './log-utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract an app folder name from an OVH Manager package name.
|
|
12
|
+
*
|
|
13
|
+
* Example:
|
|
14
|
+
* "@ovh-ux/manager-zimbra-app" → "zimbra"
|
|
15
|
+
*
|
|
16
|
+
* @param {string} packageName - Full npm package name.
|
|
17
|
+
* @returns {string|null} The extracted app folder name, or null if the format is invalid.
|
|
18
|
+
*/
|
|
19
|
+
function extractAppFolderFromPackageName(packageName) {
|
|
20
|
+
const match = packageName.match(/^@ovh-ux\/manager-(.+)-app$/);
|
|
21
|
+
if (!match) {
|
|
22
|
+
logError(`❌ Invalid package name format: ${packageName}`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return match[1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse CLI arguments for `--apps` or `--app` flags.
|
|
30
|
+
*
|
|
31
|
+
* Accepts a comma-separated list of app folder names. Example:
|
|
32
|
+
* `--apps zimbra,web`
|
|
33
|
+
*
|
|
34
|
+
* @param {string} appsDir - Absolute path to the apps root directory.
|
|
35
|
+
* @returns {{appFolders: string[], packageNames: string[]} | null}
|
|
36
|
+
* Object containing valid app folders and an empty `packageNames` array,
|
|
37
|
+
* or null if no `--apps`/`--app` flag is provided.
|
|
38
|
+
*/
|
|
39
|
+
function parseAppsArg(appsDir) {
|
|
40
|
+
const appsArg = process.argv.find((arg) => arg === '--apps' || arg === '--app');
|
|
41
|
+
if (!appsArg) return null;
|
|
42
|
+
|
|
43
|
+
const index = process.argv.indexOf(appsArg);
|
|
44
|
+
const rawValue = process.argv[index + 1] || '';
|
|
45
|
+
const appFolders = rawValue
|
|
46
|
+
.split(',')
|
|
47
|
+
.map((n) => n.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
|
|
50
|
+
const validFolders = appFolders.filter((folder) => {
|
|
51
|
+
if (!fs.existsSync(path.join(appsDir, folder))) {
|
|
52
|
+
logError(`❌ App not found: ${folder} → skipping`);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
logInfo(`Running in apps mode for: ${validFolders.join(', ')}`);
|
|
59
|
+
return { appFolders: validFolders, packageNames: [] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse CLI arguments for `--packages` or `--package` flags.
|
|
64
|
+
*
|
|
65
|
+
* Accepts a comma-separated list of package names. Example:
|
|
66
|
+
* `--packages @ovh-ux/manager-zimbra-app,@ovh-ux/manager-web-app`
|
|
67
|
+
*
|
|
68
|
+
* Each package name is mapped to its corresponding app folder.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} appsDir - Absolute path to the apps root directory.
|
|
71
|
+
* @returns {{appFolders: string[], packageNames: string[]} | null}
|
|
72
|
+
* Object mapping package names to discovered app folders,
|
|
73
|
+
* or null if no `--packages`/`--package` flag is provided.
|
|
74
|
+
*/
|
|
75
|
+
function parsePackagesArg(appsDir) {
|
|
76
|
+
const packagesArg = process.argv.find((arg) => arg === '--packages' || arg === '--package');
|
|
77
|
+
if (!packagesArg) return null;
|
|
78
|
+
|
|
79
|
+
const index = process.argv.indexOf(packagesArg);
|
|
80
|
+
const rawValue = process.argv[index + 1] || '';
|
|
81
|
+
const packageNames = rawValue
|
|
82
|
+
.split(',')
|
|
83
|
+
.map((n) => n.trim())
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
|
|
86
|
+
const mappedFolders = [];
|
|
87
|
+
for (const pkgName of packageNames) {
|
|
88
|
+
const folder = extractAppFolderFromPackageName(pkgName);
|
|
89
|
+
if (folder && fs.existsSync(path.join(appsDir, folder))) {
|
|
90
|
+
mappedFolders.push(folder);
|
|
91
|
+
} else {
|
|
92
|
+
logError(`❌ Package ${pkgName} → app folder ${folder} not found, skipping`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
logInfo(
|
|
97
|
+
`Running in packages mode for: ${packageNames.join(', ')} → apps: ${mappedFolders.join(', ')}`,
|
|
98
|
+
);
|
|
99
|
+
return { appFolders: mappedFolders, packageNames };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Discover all React apps in the apps directory when no CLI flags are provided.
|
|
104
|
+
*
|
|
105
|
+
* Only apps that contain a `package.json` file and are identified as React apps
|
|
106
|
+
* (via `isReactApp`) will be included.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} appsDir - Absolute path to the apps root directory.
|
|
109
|
+
* @returns {{appFolders: string[], packageNames: string[]}}
|
|
110
|
+
* Object containing discovered app folders and an empty `packageNames` array.
|
|
111
|
+
*/
|
|
112
|
+
function discoverApps(appsDir) {
|
|
113
|
+
const discoveredAppFolders = fs.readdirSync(appsDir).filter((dir) => {
|
|
114
|
+
const pkgPath = path.join(appsDir, dir, 'package.json');
|
|
115
|
+
return fs.existsSync(pkgPath) && isReactApp(pkgPath);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (discoveredAppFolders.length === 0) {
|
|
119
|
+
logWarn('⚠️ No React apps found to analyze.');
|
|
120
|
+
return { appFolders: [], packageNames: [] };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
logInfo(`Running in auto-discovery mode (${discoveredAppFolders.length} apps).`);
|
|
124
|
+
return { appFolders: discoveredAppFolders, packageNames: [] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse CLI arguments to resolve which apps or packages should be analyzed.
|
|
129
|
+
*
|
|
130
|
+
* Priority order:
|
|
131
|
+
* 1. Explicit `--apps` flag
|
|
132
|
+
* 2. Explicit `--packages` flag
|
|
133
|
+
* 3. Auto-discovery fallback
|
|
134
|
+
*
|
|
135
|
+
* @param {string} appsDir - Absolute path to the apps root directory.
|
|
136
|
+
* @returns {{appFolders: string[], packageNames: string[]}}
|
|
137
|
+
* Object containing app folder names and package names for analysis.
|
|
138
|
+
*/
|
|
139
|
+
export function parseCliTargets(appsDir) {
|
|
140
|
+
return parseAppsArg(appsDir) || parsePackagesArg(appsDir) || discoverApps(appsDir);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build CLI arguments for `typescript-coverage-report` from config.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} outputDir - Output directory for the report.
|
|
147
|
+
* @returns {string[]} Array of CLI arguments.
|
|
148
|
+
*/
|
|
149
|
+
export function buildTypesCoverageArgs(outputDir) {
|
|
150
|
+
const args = ['--outputDir', outputDir];
|
|
151
|
+
|
|
152
|
+
const mapping = {
|
|
153
|
+
threshold: '--threshold',
|
|
154
|
+
strict: '--strict',
|
|
155
|
+
debug: '--debug',
|
|
156
|
+
cache: '--cache',
|
|
157
|
+
project: '--project',
|
|
158
|
+
ignoreUnread: '--ignore-unread',
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
for (const [key, flag] of Object.entries(mapping)) {
|
|
162
|
+
const value = typesCoverageConfig[key];
|
|
163
|
+
if (value === undefined || value === false) continue;
|
|
164
|
+
|
|
165
|
+
if (typeof value === 'boolean') {
|
|
166
|
+
if (value) args.push(flag);
|
|
167
|
+
} else {
|
|
168
|
+
args.push(flag, String(value));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Handle array options separately
|
|
173
|
+
if (Array.isArray(typesCoverageConfig.ignoreFiles)) {
|
|
174
|
+
for (const pattern of typesCoverageConfig.ignoreFiles) {
|
|
175
|
+
args.push('--ignore-files', pattern);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return args;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Translate our config → jscpd CLI flags.
|
|
184
|
+
* @param {string} outputDir - output directory for jscpd reports
|
|
185
|
+
* @returns {string[]}
|
|
186
|
+
*/
|
|
187
|
+
export function buildCodeDuplicationArgs(outputDir) {
|
|
188
|
+
const cfg = codeDuplicationConfig;
|
|
189
|
+
const args = [
|
|
190
|
+
'--output',
|
|
191
|
+
outputDir,
|
|
192
|
+
'--reporters',
|
|
193
|
+
Array.isArray(cfg.reporters) ? cfg.reporters.join(',') : String(cfg.reporters || 'console'),
|
|
194
|
+
'--mode',
|
|
195
|
+
cfg.mode,
|
|
196
|
+
'--min-lines',
|
|
197
|
+
String(cfg.minLines),
|
|
198
|
+
'--min-tokens',
|
|
199
|
+
String(cfg.minTokens),
|
|
200
|
+
'--max-lines',
|
|
201
|
+
String(cfg.maxLines),
|
|
202
|
+
'--max-size',
|
|
203
|
+
String(cfg.maxSize),
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
if (cfg.threshold !== null && cfg.threshold !== undefined) {
|
|
207
|
+
args.push('--threshold', String(cfg.threshold));
|
|
208
|
+
}
|
|
209
|
+
if (cfg.absolute) {
|
|
210
|
+
args.push('--absolute');
|
|
211
|
+
}
|
|
212
|
+
if (Array.isArray(cfg.formats) && cfg.formats.length > 0) {
|
|
213
|
+
args.push('--format', cfg.formats.join(','));
|
|
214
|
+
}
|
|
215
|
+
if (Array.isArray(cfg.ignore) && cfg.ignore.length > 0) {
|
|
216
|
+
// jscpd accepts comma-separated ignore globs
|
|
217
|
+
// https://github.com/kucherenko/jscpd/blob/master/apps/jscpd/README.md#ignore
|
|
218
|
+
args.push('--ignore', cfg.ignore.join(','));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return args;
|
|
222
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { logError, logInfo } from './log-utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Safe JSON read.
|
|
8
|
+
*/
|
|
9
|
+
export function readJsonSafe(file) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Remove old combined report files (JSON & HTML).
|
|
19
|
+
*/
|
|
20
|
+
export function removeOldCombinedReports(root, dirName, jsonFile, htmlFile) {
|
|
21
|
+
const combinedJson = path.join(root, dirName, jsonFile);
|
|
22
|
+
const combinedHtml = path.join(root, dirName, htmlFile);
|
|
23
|
+
|
|
24
|
+
[combinedJson, combinedHtml].forEach((file) => {
|
|
25
|
+
if (fs.existsSync(file)) {
|
|
26
|
+
fs.rmSync(file, { force: true });
|
|
27
|
+
logInfo(`🧹 Removed old combined report ${file}`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate combined JSON & HTML reports.
|
|
34
|
+
* @param {string} reportRoot - output root
|
|
35
|
+
* @param {string} jsonFile - filename for combined JSON
|
|
36
|
+
* @param {string} htmlFile - filename for combined HTML
|
|
37
|
+
* @param {Function} collectorFn - e.g., collectTsTypesCoverage or collectPerfBudgets
|
|
38
|
+
* @param {Function} htmlGenFn - e.g., generateTsTypesCoverageHtml or generatePerfBudgetsHtml
|
|
39
|
+
*/
|
|
40
|
+
// eslint-disable-next-line max-params
|
|
41
|
+
export function generateCombinedReports(reportRoot, jsonFile, htmlFile, collectorFn, htmlGenFn) {
|
|
42
|
+
try {
|
|
43
|
+
const combinedJsonPath = path.join(reportRoot, jsonFile);
|
|
44
|
+
const combinedHtmlPath = path.join(reportRoot, htmlFile);
|
|
45
|
+
|
|
46
|
+
const summary = collectorFn(reportRoot);
|
|
47
|
+
fs.writeFileSync(combinedJsonPath, JSON.stringify(summary, null, 2));
|
|
48
|
+
fs.writeFileSync(combinedHtmlPath, htmlGenFn(summary));
|
|
49
|
+
} catch (err) {
|
|
50
|
+
logError(`❌ Failed to generate combined reports: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log an informational message with a standard prefix.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} msg - The message to log.
|
|
5
|
+
* @returns {void}
|
|
6
|
+
*/
|
|
7
|
+
export function logInfo(msg) {
|
|
8
|
+
console.log(`[manager-static-analysis-kit] ${msg}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Log a warning message with a standard prefix.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} msg - The message to log.
|
|
15
|
+
* @returns {void}
|
|
16
|
+
*/
|
|
17
|
+
export function logWarn(msg) {
|
|
18
|
+
console.warn(`⚠️ [manager-static-analysis-kit] ${msg}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Log an error message with a standard prefix.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} msg - The message to log.
|
|
25
|
+
* @returns {void}
|
|
26
|
+
*/
|
|
27
|
+
export function logError(msg) {
|
|
28
|
+
console.error(`❌ [manager-static-analysis-kit] ${msg}`);
|
|
29
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
|
|
6
|
+
import { resolveAppInfo } from './apps-utils.js';
|
|
7
|
+
import { generateCombinedReports } from './file-utils.js';
|
|
8
|
+
import { logError, logInfo } from './log-utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ensure the given binary exists.
|
|
12
|
+
*/
|
|
13
|
+
export function ensureBinExists(binPath, label) {
|
|
14
|
+
if (!fs.existsSync(binPath)) {
|
|
15
|
+
logError(`❌ ${label} binary not found at ${binPath}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run a child process with consistent defaults.
|
|
22
|
+
*/
|
|
23
|
+
export function runCommand(bin, args, cwd) {
|
|
24
|
+
const result = spawnSync(bin, args, {
|
|
25
|
+
cwd,
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
shell: true,
|
|
28
|
+
});
|
|
29
|
+
return result.status === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generic per-app analysis runner with safety.
|
|
34
|
+
*/
|
|
35
|
+
export function runAppsAnalysis({
|
|
36
|
+
appsDir,
|
|
37
|
+
appFolders,
|
|
38
|
+
requireReact = false,
|
|
39
|
+
binaryLabel,
|
|
40
|
+
appRunner, // (appDir, appShortName, appInfo) => boolean
|
|
41
|
+
reportsRootDirName,
|
|
42
|
+
combinedJson,
|
|
43
|
+
combinedHtml,
|
|
44
|
+
collectFn,
|
|
45
|
+
generateHtmlFn,
|
|
46
|
+
outputRootDir,
|
|
47
|
+
}) {
|
|
48
|
+
const analyzed = [];
|
|
49
|
+
|
|
50
|
+
for (const appFolder of appFolders) {
|
|
51
|
+
const appInfo = resolveAppInfo(appsDir, appFolder, { requireReact });
|
|
52
|
+
if (!appInfo) continue;
|
|
53
|
+
|
|
54
|
+
let ok = false;
|
|
55
|
+
try {
|
|
56
|
+
ok = appRunner(appInfo.appDir, appInfo.appShortName, appInfo);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
logError(
|
|
59
|
+
`❌ Fatal error while analyzing ${appInfo.appShortName}: ${err.stack || err.message}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (ok) analyzed.push(appInfo.appShortName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (analyzed.length > 0) {
|
|
66
|
+
if (typeof collectFn !== 'function' || typeof generateHtmlFn !== 'function') {
|
|
67
|
+
logError(`❌ Invalid collector or HTML generator passed to ${binaryLabel} analysis`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
generateCombinedReports(
|
|
72
|
+
path.join(outputRootDir, reportsRootDirName),
|
|
73
|
+
combinedJson,
|
|
74
|
+
combinedHtml,
|
|
75
|
+
collectFn,
|
|
76
|
+
generateHtmlFn,
|
|
77
|
+
);
|
|
78
|
+
logInfo(`✅ ${binaryLabel} reports generated (per-app + combined).`);
|
|
79
|
+
} else {
|
|
80
|
+
logError('❌ No apps successfully analyzed. Exiting.');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return analyzed;
|
|
85
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { logError, logInfo, logWarn } from './log-utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert app folder names into Turbo filters by resolving their package.json names.
|
|
9
|
+
*
|
|
10
|
+
* Each app folder must contain a valid `package.json`. If not found, the app is skipped.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} appsDir - Absolute path to the apps root directory.
|
|
13
|
+
* @param {string[]} appFolders - List of app folder names inside the apps directory.
|
|
14
|
+
* @returns {string[]} Array of package names to be used as Turbo filters.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // appsDir = "/workspace/manager/apps"
|
|
18
|
+
* // appFolders = ["zimbra", "web"]
|
|
19
|
+
* // Returns: ["@ovh-ux/manager-zimbra-app", "@ovh-ux/manager-web-app"]
|
|
20
|
+
*/
|
|
21
|
+
function resolveTurboFiltersFromAppFolders(appsDir, appFolders) {
|
|
22
|
+
return appFolders
|
|
23
|
+
.map((appFolder) => {
|
|
24
|
+
const pkgPath = path.join(appsDir, appFolder, 'package.json');
|
|
25
|
+
if (!fs.existsSync(pkgPath)) {
|
|
26
|
+
logError(`❌ No package.json for app folder ${appFolder}, skipping`);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
30
|
+
return pkg.name;
|
|
31
|
+
})
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve Turbo filters (package names) based on input mode.
|
|
37
|
+
*
|
|
38
|
+
* - If only `appFolders` are provided, resolves their corresponding package names.
|
|
39
|
+
* - If `packageNames` are provided, they are used directly as filters.
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} options
|
|
42
|
+
* @param {string} options.appsDir - Absolute path to the apps root directory.
|
|
43
|
+
* @param {string[]} options.appFolders - List of app folders to analyze.
|
|
44
|
+
* @param {string[]} options.packageNames - List of package names (overrides appFolders).
|
|
45
|
+
* @returns {string[]} List of resolved Turbo filters (package names).
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* resolveTurboFilters({
|
|
49
|
+
* appsDir: "/workspace/manager/apps",
|
|
50
|
+
* appFolders: ["zimbra"],
|
|
51
|
+
* packageNames: []
|
|
52
|
+
* });
|
|
53
|
+
* // → ["@ovh-ux/manager-zimbra-app"]
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* resolveTurboFilters({
|
|
57
|
+
* appsDir: "/workspace/manager/apps",
|
|
58
|
+
* appFolders: [],
|
|
59
|
+
* packageNames: ["@ovh-ux/manager-web-app"]
|
|
60
|
+
* });
|
|
61
|
+
* // → ["@ovh-ux/manager-web-app"]
|
|
62
|
+
*/
|
|
63
|
+
export function resolveTurboFilters({ appsDir, appFolders, packageNames }) {
|
|
64
|
+
let filters = [];
|
|
65
|
+
|
|
66
|
+
if (appFolders.length > 0 && packageNames.length === 0) {
|
|
67
|
+
filters = resolveTurboFiltersFromAppFolders(appsDir, appFolders);
|
|
68
|
+
} else if (packageNames.length > 0) {
|
|
69
|
+
filters = packageNames;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return filters;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Run a Turbo build for the provided filters.
|
|
77
|
+
*
|
|
78
|
+
* - Executes `turbo run build` with `--filter` for each package name.
|
|
79
|
+
* - Runs in the specified root directory.
|
|
80
|
+
* - If no filters are provided, the build step is skipped with a warning.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} rootDir - Root directory where Turbo should be executed.
|
|
83
|
+
* @param {string[]} filters - List of Turbo build filters (package names).
|
|
84
|
+
* @returns {void}
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* runTurboBuild("/workspace", ["@ovh-ux/manager-zimbra-app"]);
|
|
88
|
+
* // Executes: turbo run build --filter @ovh-ux/manager-zimbra-app
|
|
89
|
+
*/
|
|
90
|
+
export function runTurboBuild(rootDir, filters) {
|
|
91
|
+
if (filters.length === 0) {
|
|
92
|
+
logWarn('⚠️ No Turbo build filters provided, skipping build step.');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const turboArgs = ['run', 'build', ...filters.flatMap((pkgName) => ['--filter', pkgName])];
|
|
97
|
+
logInfo(`Building with Turbo (filters: ${filters.join(', ')})`);
|
|
98
|
+
|
|
99
|
+
const buildResult = spawnSync('turbo', turboArgs, {
|
|
100
|
+
cwd: rootDir,
|
|
101
|
+
stdio: 'inherit',
|
|
102
|
+
shell: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (buildResult.status !== 0) {
|
|
106
|
+
logWarn('⚠️ Turbo build failed for one or more targets. Will still try to analyze apps.');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Run Turbo tests with optional filters and extra arguments.
|
|
112
|
+
*
|
|
113
|
+
* This function spawns a synchronous Turbo process to run tests,
|
|
114
|
+
* applying filters for specific packages if provided. It logs
|
|
115
|
+
* progress and warns if any tests fail, but does not throw.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} rootDir - Root directory to run Turbo in (typically the monorepo root).
|
|
118
|
+
* @param {string[]} filters - List of package filters (Turbo `--filter` flags).
|
|
119
|
+
* @param {string[]} [extraArgs=[]] - Additional arguments to forward to Turbo.
|
|
120
|
+
* @returns {void}
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* // Run tests for "web" and "zimbra" apps with coverage
|
|
124
|
+
* runTurboTests(process.cwd(), ['web', 'zimbra'], ['--', '--coverage']);
|
|
125
|
+
*/
|
|
126
|
+
export function runTurboTests(rootDir, filters, extraArgs = []) {
|
|
127
|
+
const args = ['run', 'test', ...filters.flatMap((pkg) => ['--filter', pkg]), ...extraArgs];
|
|
128
|
+
logInfo(`Testing with Turbo (filters: ${filters.join(', ')})`);
|
|
129
|
+
|
|
130
|
+
const res = spawnSync('turbo', args, {
|
|
131
|
+
cwd: rootDir,
|
|
132
|
+
stdio: 'inherit',
|
|
133
|
+
shell: true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (res.status !== 0) {
|
|
137
|
+
logWarn('⚠️ Turbo tests failed for one or more targets. Will still harvest coverage if any.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CombinedCodeDuplicationSummary, JscpdReportV4, WorstCodeDuplicationFile } from '../types/CodeDuplication.js';
|
|
2
|
+
export declare const extPriority: (file: string) => number;
|
|
3
|
+
export declare function extractWorstFiles(report: JscpdReportV4): WorstCodeDuplicationFile[];
|
|
4
|
+
export declare function collectCodeDuplication(reportOutputDir: string): CombinedCodeDuplicationSummary;
|
|
5
|
+
export declare function generateCodeDuplicationHtml(summary: CombinedCodeDuplicationSummary): string;
|
|
6
|
+
//# sourceMappingURL=code-duplication-analysis-helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-duplication-analysis-helper.d.ts","sourceRoot":"","sources":["../../../../src/adapters/code-duplication/helpers/code-duplication-analysis-helper.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,8BAA8B,EAC9B,aAAa,EACb,wBAAwB,EAEzB,MAAM,0BAA0B,CAAC;AAUlC,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,MAc1C,CAAC;AAkEF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,wBAAwB,EAAE,CAsEnF;AAkED,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,GAAG,8BAA8B,CA+B9F;AAyGD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,8BAA8B,GAAG,MAAM,CAmC3F"}
|