@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
@@ -1,39 +1,36 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync } from 'node:child_process';
3
2
  import fs from 'node:fs';
4
3
  import path from 'node:path';
5
4
  import process from 'node:process';
6
- import { fileURLToPath } from 'node:url';
7
5
 
8
6
  import {
9
7
  collectPerfBudgets,
10
8
  generatePerfBudgetsHtml,
11
9
  } from '../dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.js';
12
10
  import { bundleAnalysisConfig } from '../dist/configs/bundle-analysis-config.js';
13
- import { isReactApp, logError, logInfo, logWarn } from './cli-utils.js';
14
-
15
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
- const rootDir = path.resolve(__dirname, '../../..');
17
- const outputRootDir = path.resolve(__dirname, '../../../..');
18
- const appsDir = path.join(rootDir, 'manager/apps');
19
- const bundleAnalyzerBin = path.resolve(__dirname, '../node_modules/.bin/analyze');
20
- const perfBudgetsReportDirName = 'perf-budgets-reports';
21
- const perfBudgetCombinedJsonReportName = 'perf-budgets-combined-report.json';
22
- const perfBudgetCombinedHtmlReportName = 'perf-budgets-combined-report.html';
23
-
24
- // Shared packages that must always be built
25
- const sharedPackages = [
26
- {
27
- name: '@ovh-ux/manager-react-components',
28
- folder: 'manager-react-components',
29
- path: path.join(rootDir, 'packages/manager-react-components'),
30
- },
31
- ];
11
+ import {
12
+ appsDir,
13
+ bundleAnalyzerBinPath,
14
+ outputRootDir,
15
+ perfBudgetCombinedHtmlReportName,
16
+ perfBudgetCombinedJsonReportName,
17
+ perfBudgetsReportDirName,
18
+ rootDir,
19
+ } from './cli-path-config.js';
20
+ import { parseCliTargets } from './utils/args-parse-utils.js';
21
+ import { logError, logInfo, logWarn } from './utils/log-utils.js';
22
+ import { ensureBinExists, runAppsAnalysis, runCommand } from './utils/runner-utils.js';
23
+ import { resolveTurboFilters, runTurboBuild } from './utils/turbo-utils.js';
32
24
 
33
25
  /**
34
- * Run vite-bundle-analyzer for given mode (html or json).
26
+ * Run vite-bundle-analyzer for a given mode (html or json).
27
+ *
28
+ * @param {string} appDir - Absolute app directory
29
+ * @param {string} appShortName - Shortened app name
30
+ * @param {"static"|"json"} mode - Analyzer mode
31
+ * @returns {boolean} True if analyzer produced output, false otherwise
35
32
  */
36
- function runBundleAnalyzer(appDirectory, appShortName, mode) {
33
+ function runAppBundleAnalyzer(appDir, appShortName, mode) {
37
34
  const reportFile = `${bundleAnalysisConfig?.reportFile || 'bundle-report'}${
38
35
  mode === 'json' ? '.json' : '.html'
39
36
  }`;
@@ -50,306 +47,70 @@ function runBundleAnalyzer(appDirectory, appShortName, mode) {
50
47
  bundleAnalysisConfig?.reportFile || 'bundle-report',
51
48
  ];
52
49
 
53
- const result = spawnSync(bundleAnalyzerBin, analyzerArgs, {
54
- cwd: appDirectory,
55
- stdio: 'inherit',
56
- shell: true,
57
- });
58
-
59
- if (result.status !== 0) {
50
+ const ok = runCommand(bundleAnalyzerBinPath, analyzerArgs, appDir);
51
+ if (!ok) {
60
52
  logError(`❌ Analyzer (${mode}) failed for ${appShortName}`);
61
- return;
53
+ return false;
62
54
  }
63
55
 
64
- const distReport = path.join(appDirectory, 'dist', reportFile);
56
+ const distReport = path.join(appDir, 'dist', reportFile);
65
57
  const targetReport = path.join(outputDir, reportFile);
66
58
 
67
59
  if (fs.existsSync(distReport)) {
68
60
  fs.copyFileSync(distReport, targetReport);
69
61
  logInfo(`✅ ${mode.toUpperCase()} report copied to ${targetReport}`);
70
- return targetReport;
62
+ return true;
71
63
  } else {
72
64
  logWarn(`⚠️ Expected ${mode} report not found at ${distReport}`);
65
+ return false;
73
66
  }
74
67
  }
75
68
 
76
69
  /**
77
- * Build selected targets with Turbo.
78
- */
79
- function runTurboBuild(filters) {
80
- if (filters.length === 0) {
81
- logWarn('⚠️ No Turbo build filters provided, skipping build step.');
82
- return;
83
- }
84
-
85
- const turboArgs = ['run', 'build', ...filters.flatMap((pkgName) => ['--filter', pkgName])];
86
- logInfo(`Building with Turbo (filters: ${filters.join(', ')})`);
87
-
88
- const buildResult = spawnSync('turbo', turboArgs, {
89
- cwd: rootDir,
90
- stdio: 'inherit',
91
- shell: true,
92
- });
93
-
94
- if (buildResult.status !== 0) {
95
- logWarn('⚠️ Turbo build failed for one or more targets. Will still try to analyze apps.');
96
- }
97
- }
98
-
99
- /**
100
- * Analyze all given app folders.
101
- */
102
- function analyzeAppFolders(appFolders) {
103
- const analyzed = [];
104
- for (const appFolder of appFolders) {
105
- try {
106
- const appDir = path.join(appsDir, appFolder);
107
- if (!fs.existsSync(appDir)) {
108
- logError(`❌ App folder not found: ${appFolder} → skipping`);
109
- continue;
110
- }
111
-
112
- const pkg = JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf-8'));
113
- const appName = pkg.name;
114
- const appShortName = appName.replace(/^@ovh-ux\//, '');
115
-
116
- runBundleAnalyzer(appDir, appShortName, 'static');
117
- runBundleAnalyzer(appDir, appShortName, 'json');
118
-
119
- analyzed.push(appName);
120
- } catch (err) {
121
- logError(`❌ Failed to analyze ${appFolder}: ${err.message}`);
122
- }
123
- }
124
- return analyzed;
125
- }
126
-
127
- /**
128
- * Remove old combined perf budget reports.
129
- */
130
- function removeOldCombinedReports() {
131
- const combinedJsonPath = path.join(
132
- outputRootDir,
133
- perfBudgetsReportDirName,
134
- perfBudgetCombinedJsonReportName,
135
- );
136
- const combinedHtmlPath = path.join(
137
- outputRootDir,
138
- perfBudgetsReportDirName,
139
- perfBudgetCombinedHtmlReportName,
140
- );
141
-
142
- [combinedJsonPath, combinedHtmlPath].forEach((file) => {
143
- if (fs.existsSync(file)) {
144
- fs.unlinkSync(file);
145
- logInfo(`🗑️ Removed old report: ${file}`);
146
- }
147
- });
148
- }
149
-
150
- /**
151
- * Collect budgets and regenerate combined reports.
152
- */
153
- function generateCombinedReports(root) {
154
- removeOldCombinedReports();
155
-
156
- const summary = collectPerfBudgets(root);
157
-
158
- const combinedJsonPath = path.join(
159
- root,
160
- perfBudgetsReportDirName,
161
- perfBudgetCombinedJsonReportName,
162
- );
163
- fs.writeFileSync(combinedJsonPath, JSON.stringify(summary, null, 2));
164
- logInfo(`✅ Combined JSON report written to ${combinedJsonPath}`);
165
-
166
- const combinedHtmlPath = path.join(
167
- root,
168
- perfBudgetsReportDirName,
169
- perfBudgetCombinedHtmlReportName,
170
- );
171
- fs.writeFileSync(combinedHtmlPath, generatePerfBudgetsHtml(summary));
172
- logInfo(`✅ Combined HTML report written to ${combinedHtmlPath}`);
173
- }
174
-
175
- /**
176
- * Guess app folder name from package name.
177
- * Example: @ovh-ux/manager-zimbra-app -> zimbra
178
- */
179
- function extractAppFolderFromPackageName(packageName) {
180
- const match = packageName.match(/^@ovh-ux\/manager-(.+)-app$/);
181
- if (!match) {
182
- logError(`❌ Invalid package name format: ${packageName}`);
183
- return null;
184
- }
185
- return match[1];
186
- }
187
-
188
- /**
189
- * Convert app folders to Turbo filters (package names).
190
- */
191
- function resolveTurboFiltersFromAppFolders(appFolders) {
192
- return appFolders
193
- .map((appFolder) => {
194
- const pkgPath = path.join(appsDir, appFolder, 'package.json');
195
- if (!fs.existsSync(pkgPath)) {
196
- logError(`❌ No package.json for app folder ${appFolder}, skipping`);
197
- return null;
198
- }
199
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
200
- return pkg.name;
201
- })
202
- .filter(Boolean);
203
- }
204
-
205
- /**
206
- * Parse CLI arguments for `--apps` or `--app` flags.
207
- *
208
- * @returns {{appFolders: string[], packageNames: string[]} | null}
209
- * An object with valid app folder names and an empty packageNames array,
210
- * or null if the flag is not present.
211
- */
212
- function parseAppsArg() {
213
- const appsArg = process.argv.find((arg) => arg === '--apps' || arg === '--app');
214
- if (!appsArg) return null;
215
-
216
- const index = process.argv.indexOf(appsArg);
217
- const rawValue = process.argv[index + 1] || '';
218
- const appFolders = rawValue
219
- .split(',')
220
- .map((n) => n.trim())
221
- .filter(Boolean);
222
-
223
- const validFolders = appFolders.filter((folder) => {
224
- if (!fs.existsSync(path.join(appsDir, folder))) {
225
- logError(`❌ App not found: ${folder} → skipping`);
226
- return false;
227
- }
228
- return true;
229
- });
230
-
231
- logInfo(`Running in apps mode for: ${validFolders.join(', ')}`);
232
- return { appFolders: validFolders, packageNames: [] };
233
- }
234
-
235
- /**
236
- * Parse CLI arguments for `--packages` or `--package` flags.
237
- *
238
- * @returns {{appFolders: string[], packageNames: string[]} | null}
239
- * An object mapping package names to their corresponding app folders,
240
- * or null if the flag is not present.
241
- */
242
- function parsePackagesArg() {
243
- const packagesArg = process.argv.find((arg) => arg === '--packages' || arg === '--package');
244
- if (!packagesArg) return null;
245
-
246
- const index = process.argv.indexOf(packagesArg);
247
- const rawValue = process.argv[index + 1] || '';
248
- const packageNames = rawValue
249
- .split(',')
250
- .map((n) => n.trim())
251
- .filter(Boolean);
252
-
253
- const mappedFolders = [];
254
- for (const pkgName of packageNames) {
255
- const folder = extractAppFolderFromPackageName(pkgName);
256
- if (folder && fs.existsSync(path.join(appsDir, folder))) {
257
- mappedFolders.push(folder);
258
- } else {
259
- logError(`❌ Package ${pkgName} → app folder ${folder} not found, skipping`);
260
- }
261
- }
262
-
263
- logInfo(
264
- `Running in packages mode for: ${packageNames.join(', ')} → apps: ${mappedFolders.join(', ')}`,
265
- );
266
- return { appFolders: mappedFolders, packageNames };
267
- }
268
-
269
- /**
270
- * Discover all React apps automatically when no CLI flags are provided.
70
+ * Run perf budgets analysis for one app (both static + json).
271
71
  *
272
- * @returns {{appFolders: string[], packageNames: string[]}}
273
- * An object containing discovered React app folders and an empty packageNames array.
72
+ * @param {string} appDir
73
+ * @param {string} appShortName
74
+ * @returns {boolean} True if at least one report succeeded
274
75
  */
275
- function discoverApps() {
276
- const discoveredAppFolders = fs.readdirSync(appsDir).filter((dir) => {
277
- const pkgPath = path.join(appsDir, dir, 'package.json');
278
- return fs.existsSync(pkgPath) && isReactApp(pkgPath);
279
- });
280
-
281
- if (discoveredAppFolders.length === 0) {
282
- logWarn('⚠️ No React apps found to analyze.');
283
- return { appFolders: [], packageNames: [] };
284
- }
285
-
286
- logInfo(`Running in auto-discovery mode (${discoveredAppFolders.length} apps).`);
287
- return { appFolders: discoveredAppFolders, packageNames: [] };
76
+ function runAppPerfBudgets(appDir, appShortName) {
77
+ const okStatic = runAppBundleAnalyzer(appDir, appShortName, 'static');
78
+ const okJson = runAppBundleAnalyzer(appDir, appShortName, 'json');
79
+ return okStatic || okJson;
288
80
  }
289
81
 
290
82
  /**
291
- * Parse CLI arguments to resolve which apps or packages should be analyzed.
83
+ * Main CLI entrypoint for perf budgets analysis.
292
84
  *
293
- * Priority:
294
- * 1. Explicit `--apps`
295
- * 2. Explicit `--packages`
296
- * 3. Auto-discovery fallback
297
- *
298
- * @returns {{appFolders: string[], packageNames: string[]}}
299
- * Object containing app folder names and package names for analysis.
300
- */
301
- function parseCliTargets() {
302
- return parseAppsArg() || parsePackagesArg() || discoverApps();
303
- }
304
-
305
- /**
306
- * Resolve Turbo filters (package names) depending on mode.
307
- */
308
- function resolveTurboFilters({ appFolders, packageNames }) {
309
- let filters = [];
310
- if (appFolders.length > 0 && packageNames.length === 0) {
311
- filters = resolveTurboFiltersFromAppFolders(appFolders);
312
- } else if (packageNames.length > 0) {
313
- filters = packageNames;
314
- }
315
-
316
- // Always include shared packages for build
317
- for (const pkg of sharedPackages) {
318
- if (fs.existsSync(pkg.path) && !filters.includes(pkg.name)) {
319
- filters.push(pkg.name);
320
- logInfo(`Including shared dependency in build: ${pkg.name}`);
321
- }
322
- }
323
-
324
- return filters;
325
- }
326
-
327
- /**
328
- * Main CLI entrypoint.
85
+ * - Ensures analyzer binary is available
86
+ * - Runs a turbo build for the selected apps
87
+ * - Executes analyzer per app via {@link runAppPerfBudgets}
88
+ * - Generates combined JSON + HTML reports
329
89
  */
330
90
  function main() {
331
91
  try {
332
- if (!fs.existsSync(bundleAnalyzerBin)) {
333
- logError(`❌ Analyzer binary not found at ${bundleAnalyzerBin}`);
334
- return;
335
- }
92
+ ensureBinExists(bundleAnalyzerBinPath, 'vite-bundle-analyzer');
336
93
 
337
- const { appFolders, packageNames } = parseCliTargets();
338
- const turboFilters = resolveTurboFilters({ appFolders, packageNames });
94
+ const { appFolders, packageNames } = parseCliTargets(appsDir);
95
+ const turboFilters = resolveTurboFilters({ appsDir, appFolders, packageNames });
339
96
 
340
97
  // Step 1: build
341
- runTurboBuild(turboFilters);
342
-
343
- // Step 2: analyze
344
- const analyzedApps = analyzeAppFolders(appFolders);
345
-
346
- // Step 3: combined reports
347
- if (analyzedApps.length > 0) {
348
- generateCombinedReports(outputRootDir);
349
- } else {
350
- logError('❌ No apps successfully analyzed. Exiting.');
351
- process.exit(1);
352
- }
98
+ runTurboBuild(rootDir, turboFilters);
99
+
100
+ // Step 2: analyze apps + combined
101
+ runAppsAnalysis({
102
+ appsDir,
103
+ appFolders,
104
+ requireReact: false,
105
+ binaryLabel: 'Perf budgets',
106
+ appRunner: runAppPerfBudgets,
107
+ reportsRootDirName: perfBudgetsReportDirName,
108
+ combinedJson: perfBudgetCombinedJsonReportName,
109
+ combinedHtml: perfBudgetCombinedHtmlReportName,
110
+ collectFn: collectPerfBudgets,
111
+ generateHtmlFn: generatePerfBudgetsHtml,
112
+ outputRootDir,
113
+ });
353
114
  } catch (err) {
354
115
  logError(`Fatal error: ${err.stack || err.message}`);
355
116
  process.exit(1);
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
- import process from 'node:process';
4
3
 
5
- import { logError, logInfo } from './cli-utils.js';
4
+ import { logError, logInfo } from './utils/log-utils.js';
6
5
 
7
6
  // Define the set of validation test cases
8
7
  const cases = [
@@ -68,7 +67,6 @@ for (const test of cases) {
68
67
 
69
68
  if (failed > 0) {
70
69
  logError(`\n❌ ${failed} test(s) failed.`);
71
- process.exit(1);
72
70
  } else {
73
71
  logInfo('\n🎉 All perf-budgets CLI validation tests passed!');
74
72
  }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { writeFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import process from 'node:process';
6
+
7
+ import { outputRootDir } from './cli-path-config.js';
8
+
9
+ const qualityCheckEntryFile = resolve(outputRootDir, 'index.html');
10
+
11
+ const isTests = process.argv.includes('--tests');
12
+
13
+ const mainScripts = [
14
+ 'manager-code-duplication',
15
+ 'manager-types-coverage',
16
+ 'manager-tests-coverage',
17
+ 'manager-perf-budgets',
18
+ ];
19
+
20
+ const testScripts = [
21
+ 'manager-code-duplication-tests',
22
+ 'manager-types-coverage-tests',
23
+ 'manager-tests-coverage-tests',
24
+ 'manager-perf-budgets-tests',
25
+ ];
26
+
27
+ const scripts = isTests ? testScripts : mainScripts;
28
+
29
+ const reports = [
30
+ {
31
+ name: `Code Duplication${isTests ? ' (Tests)' : ''}`,
32
+ folder: 'code-duplication-reports',
33
+ base: 'code-duplication-combined-report',
34
+ },
35
+ {
36
+ name: `Performance Budgets${isTests ? ' (Tests)' : ''}`,
37
+ folder: 'perf-budgets-reports',
38
+ base: 'perf-budgets-combined-report',
39
+ },
40
+ {
41
+ name: `Tests Coverage${isTests ? ' (Tests)' : ''}`,
42
+ folder: 'tests-coverage-reports',
43
+ base: 'tests-coverage-combined-report',
44
+ },
45
+ {
46
+ name: `Types Coverage${isTests ? ' (Tests)' : ''}`,
47
+ folder: 'types-coverage-reports',
48
+ base: 'types-coverage-combined-report',
49
+ },
50
+ ];
51
+
52
+ console.log(`🚀 Running static & dynamic quality ${isTests ? 'tests' : 'checks'}...\n`);
53
+
54
+ let remaining = scripts.length;
55
+ let failed = false;
56
+ const statuses = {};
57
+
58
+ function generateEntryReport() {
59
+ const links = reports
60
+ .map((report) => {
61
+ const status = statuses[report.name] ?? 'unknown';
62
+ const badge =
63
+ status === 'success'
64
+ ? '<span class="badge success">✅ Success</span>'
65
+ : status === 'fail'
66
+ ? '<span class="badge fail">❌ Failed</span>'
67
+ : '<span class="badge unknown">❔ Unknown</span>';
68
+
69
+ return `
70
+ <li>
71
+ <strong>${report.name}</strong> ${badge}<br/>
72
+ <a href="./${report.folder}/${report.base}.html">📊 HTML Report</a> |
73
+ <a href="./${report.folder}/${report.base}.json">📄 JSON Report</a>
74
+ </li>`;
75
+ })
76
+ .join('\n');
77
+
78
+ const html = `<!DOCTYPE html>
79
+ <html>
80
+ <head>
81
+ <meta charset="UTF-8">
82
+ <title>Static & Dynamic Quality ${isTests ? 'Tests' : 'Checks'} Reports</title>
83
+ <style>
84
+ body { font-family: Arial, sans-serif; margin: 2rem; background: #f9f9f9; color: #333; }
85
+ h1 { color: #2c3e50; }
86
+ p.note { font-size: 0.9rem; color: #666; margin-bottom: 1.5rem; }
87
+ ul { list-style-type: none; padding: 0; }
88
+ li { margin: 1rem 0; padding: 1rem; background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
89
+ a { text-decoration: none; margin-right: 1rem; }
90
+ .badge { padding: 0.2rem 0.6rem; border-radius: 6px; font-size: 0.85rem; margin-left: 0.5rem; }
91
+ .success { background: #2ecc71; color: #fff; }
92
+ .fail { background: #e74c3c; color: #fff; }
93
+ .unknown { background: #95a5a6; color: #fff; }
94
+ </style>
95
+ </head>
96
+ <body>
97
+ <h1>Static & Dynamic Quality ${isTests ? 'Tests' : 'Checks'} Reports</h1>
98
+ <p class="note">ℹ️ Status badges below reflect the <strong>script execution status</strong> (whether the CLI completed without error), <br/>not the actual content or results of the analysis.</p>
99
+ <ul>
100
+ ${links}
101
+ </ul>
102
+ </body>
103
+ </html>`;
104
+
105
+ writeFileSync(qualityCheckEntryFile, html, 'utf-8');
106
+ console.log(`📄 Entry report generated at ${qualityCheckEntryFile}`);
107
+ }
108
+
109
+ scripts.forEach((cmd, idx) => {
110
+ const child = spawn(cmd, { stdio: 'inherit', shell: true });
111
+
112
+ child.on('exit', (code) => {
113
+ console.log(`\n${code === 0 ? '✅' : '❌'} ${cmd} finished with code ${code}`);
114
+ if (code === 0) {
115
+ statuses[reports[idx].name] = 'success';
116
+ } else {
117
+ statuses[reports[idx].name] = 'fail';
118
+ failed = true;
119
+ }
120
+ remaining -= 1;
121
+
122
+ if (remaining === 0) {
123
+ console.log(
124
+ failed
125
+ ? '\n⚠️ Some quality scripts failed, but reports will still be generated.'
126
+ : `\n🎉 All quality ${isTests ? 'tests' : 'checks'} completed successfully`,
127
+ );
128
+
129
+ try {
130
+ generateEntryReport();
131
+ } catch (err) {
132
+ console.error('❌ Failed to generate entry report:', err);
133
+ }
134
+
135
+ process.exit(failed ? 1 : 0);
136
+ }
137
+ });
138
+ });
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ import { logError, logInfo } from './utils/log-utils.js';
5
+
6
+ const cases = [
7
+ // Debug only — very slow, runs across all apps
8
+ // { name: 'Auto-discovery (all apps)', cmd: 'yarn manager-tests-coverage', expect: 0 },
9
+
10
+ { name: 'Single app (zimbra)', cmd: 'yarn manager-tests-coverage --app zimbra', expect: 0 },
11
+ {
12
+ name: 'Multiple apps (container,zimbra,web-ongoing-operations,key-management-service)',
13
+ cmd: 'yarn manager-tests-coverage --apps container,zimbra,web-ongoing-operations,key-management-service',
14
+ expect: 0,
15
+ },
16
+ {
17
+ name: 'Single package (container)',
18
+ cmd: 'yarn manager-tests-coverage --package @ovh-ux/manager-container-app',
19
+ expect: 0,
20
+ },
21
+ {
22
+ name: 'Multiple packages (zimbra,pci-workflow)',
23
+ cmd: 'yarn manager-tests-coverage --packages @ovh-ux/manager-zimbra-app,@ovh-ux/manager-pci-workflow-app',
24
+ expect: 0,
25
+ },
26
+ {
27
+ name: 'Mixed valid+invalid apps',
28
+ cmd: 'yarn manager-tests-coverage --apps zimbra,unknown-app,container',
29
+ expect: 0,
30
+ },
31
+ {
32
+ name: 'Mixed valid+invalid packages',
33
+ cmd: 'yarn manager-tests-coverage --packages @ovh-ux/manager-zimbra-app,@ovh-ux/manager-ghost-app',
34
+ expect: 0,
35
+ },
36
+ {
37
+ name: 'All invalid apps',
38
+ cmd: 'yarn manager-tests-coverage --apps doesnotexist1,doesnotexist2',
39
+ expect: 1,
40
+ },
41
+ {
42
+ name: 'All invalid packages',
43
+ cmd: 'yarn manager-tests-coverage --packages @ovh-ux/manager-ghost-app,@ovh-ux/manager-missing-app',
44
+ expect: 1,
45
+ },
46
+ ];
47
+
48
+ let failed = 0;
49
+
50
+ for (const test of cases) {
51
+ logInfo(`\n🔎 Running: ${test.name}`);
52
+ const [bin, ...args] = test.cmd.split(' ');
53
+ const result = spawnSync(bin, args, { stdio: 'inherit', shell: true });
54
+
55
+ if (result.status !== test.expect) {
56
+ logError(`❌ ${test.name} failed (expected exit ${test.expect}, got ${result.status})`);
57
+ failed++;
58
+ } else {
59
+ logInfo(`✅ ${test.name} passed`);
60
+ }
61
+ }
62
+
63
+ if (failed > 0) {
64
+ logError(`\n❌ ${failed} test(s) failed.`);
65
+ } else {
66
+ logInfo('\n🎉 All tests-coverage CLI validation tests passed!');
67
+ }