@ovh-ux/manager-static-analysis-kit 0.9.0 → 0.11.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 +29 -0
- package/README.md +166 -24
- package/bin/cli-path-config.js +41 -2
- package/bin/code-duplication-cli.js +14 -16
- package/bin/code-duplication-tests-cli.js +43 -6
- package/bin/perf-budgets-cli.js +15 -15
- package/bin/perf-budgets-tests-cli.js +36 -3
- package/bin/static-dynamic-quality-checks.js +2 -2
- package/bin/tests-coverage-cli-tests.js +44 -6
- package/bin/tests-coverage-cli.js +35 -25
- package/bin/types-coverage-cli.js +76 -11
- package/bin/types-coverage-tests-cli.js +44 -7
- package/bin/utils/args-parse-utils.js +77 -107
- package/bin/utils/module-utils.js +121 -0
- package/bin/utils/runner-utils.js +117 -22
- package/bin/utils/turbo-utils.js +15 -74
- package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.d.ts +4 -1
- package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.d.ts.map +1 -1
- package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.js +72 -18
- package/dist/adapters/code-duplication/types/CodeDuplication.d.ts +3 -0
- package/dist/adapters/code-duplication/types/CodeDuplication.d.ts.map +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 +8 -4
- package/dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.d.ts.map +1 -1
- package/dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.js +31 -8
- package/dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.d.ts.map +1 -1
- package/dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.js +121 -48
- package/dist/adapters/types-coverage/types/TypesCoverage.d.ts +8 -0
- package/dist/adapters/types-coverage/types/TypesCoverage.d.ts.map +1 -1
- package/dist/configs/code-duplication-config.d.ts +1 -0
- package/dist/configs/code-duplication-config.d.ts.map +1 -1
- package/dist/configs/code-duplication-config.js +2 -1
- 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 +21 -0
- package/dist/configs/tests-coverage-config.js +1 -1
- package/dist/configs/types-coverage-config.d.ts +14 -0
- package/dist/configs/types-coverage-config.d.ts.map +1 -1
- package/dist/configs/types-coverage-config.js +13 -16
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/bin/utils/apps-utils.js +0 -64
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { appsDir, librariesBasePaths, staticDiscoveredLibraries } from '../cli-path-config.js';
|
|
5
|
+
import { readJsonSafe } from './file-utils.js';
|
|
6
|
+
import { logWarn } from './log-utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Determine whether a given package directory represents a React-based module.
|
|
10
|
+
*
|
|
11
|
+
* This function inspects the `package.json` file located at `pkgPath`
|
|
12
|
+
* and checks whether any of its dependencies, devDependencies, or peerDependencies
|
|
13
|
+
* include `react`. If the file does not exist, cannot be parsed, or the
|
|
14
|
+
* dependency is missing, it returns `false`.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} pkgPath - Absolute path to a `package.json` file.
|
|
17
|
+
* @returns {boolean} `true` if the module depends on React, otherwise `false`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* isReactModule('/path/to/package.json');
|
|
21
|
+
* // → true if the package declares React as a dependency
|
|
22
|
+
*/
|
|
23
|
+
export function isReactModule(pkgPath) {
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
26
|
+
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
28
|
+
if (!pkg || typeof pkg !== 'object') return false;
|
|
29
|
+
|
|
30
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
|
|
31
|
+
return Boolean(deps.react);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
logWarn(`Failed to read or parse ${pkgPath}: ${err.message}`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Discover all valid application or library modules by scanning predefined directories.
|
|
40
|
+
*
|
|
41
|
+
* Depending on the `isLibrary` flag, this function inspects:
|
|
42
|
+
* - Libraries under: `packages/manager/core`, `packages/manager/modules`, and `packages/components`
|
|
43
|
+
* - Applications under: `manager/apps`
|
|
44
|
+
*
|
|
45
|
+
* For each folder containing a `package.json` that represents a React module,
|
|
46
|
+
* a module descriptor object is created with metadata.
|
|
47
|
+
*
|
|
48
|
+
* @param {boolean} [isLibrary=false] - Whether to scan for libraries (`true`) or apps (`false`).
|
|
49
|
+
* @returns {Array<{ fullPath: string, shortPath: string, packageName: string }>}
|
|
50
|
+
* Array of discovered module descriptors.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Discover apps
|
|
54
|
+
* discoverModules();
|
|
55
|
+
* // → [{ fullPath: '/.../manager/apps/zimbra', shortPath: 'zimbra', packageName: '@ovh-ux/manager-zimbra-app' }]
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Discover libraries
|
|
59
|
+
* discoverModules(true);
|
|
60
|
+
* // → [{ fullPath: '/.../packages/manager/core/core-shell', shortPath: 'core-shell', packageName: '@ovh-ux/manager-core-shell' }]
|
|
61
|
+
*/
|
|
62
|
+
export function discoverModules(isLibrary = false) {
|
|
63
|
+
const discovered = [];
|
|
64
|
+
const modulesPaths = isLibrary ? librariesBasePaths : [appsDir];
|
|
65
|
+
|
|
66
|
+
for (const moduleBasePath of modulesPaths) {
|
|
67
|
+
if (!fs.existsSync(moduleBasePath)) continue;
|
|
68
|
+
|
|
69
|
+
const items = fs.readdirSync(moduleBasePath, { withFileTypes: true });
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
if (item.isDirectory()) {
|
|
72
|
+
const appFolder = path.join(moduleBasePath, item.name);
|
|
73
|
+
const pkgPath = path.join(appFolder, 'package.json');
|
|
74
|
+
if (fs.existsSync(pkgPath) && isReactModule(pkgPath)) {
|
|
75
|
+
const pkgDetails = readJsonSafe(pkgPath);
|
|
76
|
+
discovered.push({
|
|
77
|
+
fullPath: appFolder,
|
|
78
|
+
shortPath: item.name,
|
|
79
|
+
packageName: pkgDetails.name,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return isLibrary ? [...discovered, ...staticDiscoveredLibraries] : discovered;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Retrieve a single module (app or library) matching a given pattern.
|
|
91
|
+
*
|
|
92
|
+
* This function searches through discovered modules (via {@link discoverModules})
|
|
93
|
+
* and returns the first one matching any of the following:
|
|
94
|
+
* - The module's absolute path (`fullPath`)
|
|
95
|
+
* - The folder name (`shortPath`)
|
|
96
|
+
* - The npm package name (`packageName`)
|
|
97
|
+
*
|
|
98
|
+
* @param {string} modulePattern - Pattern to match against module identifiers.
|
|
99
|
+
* @param {boolean} [isLibrary=false] - Whether to search within libraries (`true`) or apps (`false`).
|
|
100
|
+
* @returns {{ fullPath: string, shortPath: string, packageName: string }}
|
|
101
|
+
* The matched module descriptor, or `undefined` if no match is found.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* // Match by folder name
|
|
105
|
+
* getModule('zimbra');
|
|
106
|
+
* // → { fullPath: '/.../manager/apps/zimbra', shortPath: 'zimbra', packageName: '@ovh-ux/manager-zimbra-app' }
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* // Match by package name
|
|
110
|
+
* getModule('@ovh-ux/manager-core-shell', true);
|
|
111
|
+
* // → { fullPath: '/.../packages/manager/core/core-shell', shortPath: 'core-shell', packageName: '@ovh-ux/manager-core-shell' }
|
|
112
|
+
*/
|
|
113
|
+
export function getModule(modulePattern, isLibrary = false) {
|
|
114
|
+
const discoveredLibraries = discoverModules(isLibrary) || [];
|
|
115
|
+
return discoveredLibraries.find(
|
|
116
|
+
(library) =>
|
|
117
|
+
library.fullPath === modulePattern ||
|
|
118
|
+
library.shortPath === modulePattern ||
|
|
119
|
+
library.packageName === modulePattern,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -3,12 +3,23 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { generateCombinedReports } from './file-utils.js';
|
|
6
|
+
import { generateCombinedReports, readJsonSafe } from './file-utils.js';
|
|
8
7
|
import { logError, logInfo } from './log-utils.js';
|
|
8
|
+
import { isReactModule } from './module-utils.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Ensure
|
|
11
|
+
* Ensure that a required binary exists before running an analysis command.
|
|
12
|
+
*
|
|
13
|
+
* This function verifies that a binary file exists at the given path.
|
|
14
|
+
* If not found, it logs an error message and terminates the process
|
|
15
|
+
* with exit code `1`.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} binPath - Absolute path to the binary executable.
|
|
18
|
+
* @param {string} label - Human-readable label for the binary (used in error logs).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ensureBinExists('/usr/local/bin/jscpd', 'JSCPD');
|
|
22
|
+
* // → Exits process if binary is missing
|
|
12
23
|
*/
|
|
13
24
|
export function ensureBinExists(binPath, label) {
|
|
14
25
|
if (!fs.existsSync(binPath)) {
|
|
@@ -18,7 +29,20 @@ export function ensureBinExists(binPath, label) {
|
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
|
-
*
|
|
32
|
+
* Execute a command synchronously using the system shell.
|
|
33
|
+
*
|
|
34
|
+
* This function spawns a child process that runs a binary or command
|
|
35
|
+
* with the provided arguments, inheriting the parent process's stdio streams.
|
|
36
|
+
* It returns `true` if the command completed successfully (exit code `0`).
|
|
37
|
+
*
|
|
38
|
+
* @param {string} bin - Executable name or absolute path to run.
|
|
39
|
+
* @param {string[]} args - Command-line arguments for the executable.
|
|
40
|
+
* @param {string} cwd - Working directory where the command will be executed.
|
|
41
|
+
* @returns {boolean} `true` if the process exits with status `0`, otherwise `false`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* runCommand('eslint', ['--fix', '.'], process.cwd());
|
|
45
|
+
* // → Runs ESLint and returns true if successful
|
|
22
46
|
*/
|
|
23
47
|
export function runCommand(bin, args, cwd) {
|
|
24
48
|
const result = spawnSync(bin, args, {
|
|
@@ -30,14 +54,90 @@ export function runCommand(bin, args, cwd) {
|
|
|
30
54
|
}
|
|
31
55
|
|
|
32
56
|
/**
|
|
33
|
-
*
|
|
57
|
+
* Execute analysis for a single module.
|
|
58
|
+
*
|
|
59
|
+
* This internal helper encapsulates:
|
|
60
|
+
* - Module validation (`fullPath`, `package.json`, React module check)
|
|
61
|
+
* - Error handling for analysis execution
|
|
62
|
+
* - Delegation to a provided analysis runner
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} options - Analysis parameters.
|
|
65
|
+
* @param {Object} options.module - Module descriptor with metadata.
|
|
66
|
+
* @param {Function} options.analysisRunner - Function `(fullPath, shortPath, context) => boolean`
|
|
67
|
+
* that performs the analysis for this module.
|
|
68
|
+
* @returns {boolean} `true` if analysis succeeded for the module, otherwise `false`.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* runModuleAnalysis({
|
|
72
|
+
* module: { fullPath: '/apps/zimbra', shortPath: 'zimbra', packageName: '@ovh-ux/manager-zimbra-app' },
|
|
73
|
+
* analysisRunner: runCoverageAnalysis,
|
|
74
|
+
* });
|
|
34
75
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
76
|
+
function runModuleAnalysis({ module, analysisRunner }) {
|
|
77
|
+
const { fullPath, shortPath, packageName } = module || {};
|
|
78
|
+
const pkgPath = path.join(fullPath, 'package.json');
|
|
79
|
+
let isAnalysisComplete = false;
|
|
80
|
+
|
|
81
|
+
if (!fs.existsSync(fullPath) || !fs.existsSync(pkgPath) || !isReactModule(pkgPath)) {
|
|
82
|
+
logError(`❌ Module folder not found: ${module} → skipping`);
|
|
83
|
+
return isAnalysisComplete;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
isAnalysisComplete = analysisRunner(fullPath, shortPath, {
|
|
88
|
+
pkgPath,
|
|
89
|
+
moduleDir: fullPath,
|
|
90
|
+
moduleFolder: shortPath,
|
|
91
|
+
moduleShortName: packageName,
|
|
92
|
+
pkg: readJsonSafe(pkgPath),
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
logError(`❌ Fatal error while analyzing ${shortPath}: ${err.stack || err.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return isAnalysisComplete;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Run a complete analysis across multiple modules and generate combined reports.
|
|
103
|
+
*
|
|
104
|
+
* This function orchestrates:
|
|
105
|
+
* 1. Iterating over provided modules.
|
|
106
|
+
* 2. Running analysis for each module (via `runModuleAnalysis`).
|
|
107
|
+
* 3. Aggregating successful results.
|
|
108
|
+
* 4. Generating combined JSON and HTML reports.
|
|
109
|
+
*
|
|
110
|
+
* If no modules are successfully analyzed, the process terminates with exit code `1`.
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} options - Analysis configuration.
|
|
113
|
+
* @param {Object[]} options.modules - Array of module descriptors (`{ fullPath, shortPath, packageName }`).
|
|
114
|
+
* @param {string} options.binaryLabel - Label for the analysis type (e.g. "Type Coverage").
|
|
115
|
+
* @param {Function} options.analysisRunner - Function executed per module to run analysis logic.
|
|
116
|
+
* @param {string} options.reportsRootDirName - Directory under the output root to store reports.
|
|
117
|
+
* @param {string} options.combinedJson - Filename for the combined JSON report.
|
|
118
|
+
* @param {string} options.combinedHtml - Filename for the combined HTML report.
|
|
119
|
+
* @param {Function} options.collectFn - Function used to aggregate individual module data.
|
|
120
|
+
* @param {Function} options.generateHtmlFn - Function used to generate the combined HTML output.
|
|
121
|
+
* @param {string} options.outputRootDir - Base path where reports should be written.
|
|
122
|
+
* @returns {string[]} Array of successfully analyzed module short names.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* runModulesAnalysis({
|
|
126
|
+
* modules,
|
|
127
|
+
* binaryLabel: 'Type Coverage',
|
|
128
|
+
* analysisRunner: runTypeCoverage,
|
|
129
|
+
* reportsRootDirName: 'types-coverage-reports',
|
|
130
|
+
* combinedJson: 'types-coverage-combined-report.json',
|
|
131
|
+
* combinedHtml: 'types-coverage-combined-report.html',
|
|
132
|
+
* collectFn: collectTypeCoverage,
|
|
133
|
+
* generateHtmlFn: generateTypeCoverageHtml,
|
|
134
|
+
* outputRootDir: './reports',
|
|
135
|
+
* });
|
|
136
|
+
*/
|
|
137
|
+
export function runModulesAnalysis({
|
|
138
|
+
modules,
|
|
39
139
|
binaryLabel,
|
|
40
|
-
|
|
140
|
+
analysisRunner,
|
|
41
141
|
reportsRootDirName,
|
|
42
142
|
combinedJson,
|
|
43
143
|
combinedHtml,
|
|
@@ -45,21 +145,16 @@ export function runAppsAnalysis({
|
|
|
45
145
|
generateHtmlFn,
|
|
46
146
|
outputRootDir,
|
|
47
147
|
}) {
|
|
148
|
+
/** @type {string[]} */
|
|
48
149
|
const analyzed = [];
|
|
49
150
|
|
|
50
|
-
for (const
|
|
51
|
-
const
|
|
52
|
-
|
|
151
|
+
for (const module of modules) {
|
|
152
|
+
const isAnalysisComplete = runModuleAnalysis({
|
|
153
|
+
module,
|
|
154
|
+
analysisRunner,
|
|
155
|
+
});
|
|
53
156
|
|
|
54
|
-
|
|
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);
|
|
157
|
+
if (isAnalysisComplete) analyzed.push(module.shortPath);
|
|
63
158
|
}
|
|
64
159
|
|
|
65
160
|
if (analyzed.length > 0) {
|
package/bin/utils/turbo-utils.js
CHANGED
|
@@ -1,76 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
2
|
|
|
5
|
-
import {
|
|
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
|
-
}
|
|
3
|
+
import { logInfo, logWarn } from './log-utils.js';
|
|
74
4
|
|
|
75
5
|
/**
|
|
76
6
|
* Run a Turbo build for the provided filters.
|
|
@@ -81,19 +11,25 @@ export function resolveTurboFilters({ appsDir, appFolders, packageNames }) {
|
|
|
81
11
|
*
|
|
82
12
|
* @param {string} rootDir - Root directory where Turbo should be executed.
|
|
83
13
|
* @param {string[]} filters - List of Turbo build filters (package names).
|
|
14
|
+
* @param {string[]} [extraArgs=[]] - Additional arguments to forward to Turbo.
|
|
84
15
|
* @returns {void}
|
|
85
16
|
*
|
|
86
17
|
* @example
|
|
87
18
|
* runTurboBuild("/workspace", ["@ovh-ux/manager-zimbra-app"]);
|
|
88
19
|
* // Executes: turbo run build --filter @ovh-ux/manager-zimbra-app
|
|
89
20
|
*/
|
|
90
|
-
export function runTurboBuild(rootDir, filters) {
|
|
21
|
+
export function runTurboBuild(rootDir, filters, extraArgs = []) {
|
|
91
22
|
if (filters.length === 0) {
|
|
92
23
|
logWarn('⚠️ No Turbo build filters provided, skipping build step.');
|
|
93
24
|
return;
|
|
94
25
|
}
|
|
95
26
|
|
|
96
|
-
const turboArgs = [
|
|
27
|
+
const turboArgs = [
|
|
28
|
+
'run',
|
|
29
|
+
'build',
|
|
30
|
+
...filters.flatMap((pkgName) => ['--filter', pkgName]),
|
|
31
|
+
...extraArgs,
|
|
32
|
+
];
|
|
97
33
|
logInfo(`Building with Turbo (filters: ${filters.join(', ')})`);
|
|
98
34
|
|
|
99
35
|
const buildResult = spawnSync('turbo', turboArgs, {
|
|
@@ -124,7 +60,12 @@ export function runTurboBuild(rootDir, filters) {
|
|
|
124
60
|
* runTurboTests(process.cwd(), ['web', 'zimbra'], ['--', '--coverage']);
|
|
125
61
|
*/
|
|
126
62
|
export function runTurboTests(rootDir, filters, extraArgs = []) {
|
|
127
|
-
const args = [
|
|
63
|
+
const args = [
|
|
64
|
+
'run',
|
|
65
|
+
'test',
|
|
66
|
+
...filters.flatMap((pkgName) => ['--filter', pkgName]),
|
|
67
|
+
...extraArgs,
|
|
68
|
+
];
|
|
128
69
|
logInfo(`Testing with Turbo (filters: ${filters.join(', ')})`);
|
|
129
70
|
|
|
130
71
|
const res = spawnSync('turbo', args, {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { CombinedCodeDuplicationSummary, JscpdReportV4, WorstCodeDuplicationFile } from '../types/CodeDuplication.js';
|
|
2
2
|
export declare const extPriority: (file: string) => number;
|
|
3
|
-
export declare function extractWorstFiles(report: JscpdReportV4):
|
|
3
|
+
export declare function extractWorstFiles(report: JscpdReportV4): {
|
|
4
|
+
files: WorstCodeDuplicationFile[];
|
|
5
|
+
fileCount: number;
|
|
6
|
+
};
|
|
4
7
|
export declare function collectCodeDuplication(reportOutputDir: string): CombinedCodeDuplicationSummary;
|
|
5
8
|
export declare function generateCodeDuplicationHtml(summary: CombinedCodeDuplicationSummary): string;
|
|
6
9
|
//# sourceMappingURL=code-duplication-analysis-helper.d.ts.map
|
|
@@ -1 +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;
|
|
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;AAmEF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG;IACxD,KAAK,EAAE,wBAAwB,EAAE,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB,CAwEA;AA4ED,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,GAAG,8BAA8B,CA+B9F;AAsGD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,8BAA8B,GAAG,MAAM,CAyD3F"}
|
|
@@ -32,7 +32,7 @@ function shortPathFromAppsRoot(filePath, app) {
|
|
|
32
32
|
export function extractWorstFiles(report) {
|
|
33
33
|
const dups = Array.isArray(report?.duplicates) ? report.duplicates : [];
|
|
34
34
|
if (dups.length === 0)
|
|
35
|
-
return [];
|
|
35
|
+
return { files: [], fileCount: 0 };
|
|
36
36
|
const { threshold, count } = codeDuplicationConfig.worstFiles;
|
|
37
37
|
const fileMap = {};
|
|
38
38
|
for (const dup of dups) {
|
|
@@ -56,7 +56,7 @@ export function extractWorstFiles(report) {
|
|
|
56
56
|
const estTotal = fileMap[file].estimatedTotal;
|
|
57
57
|
fileMap[file].total = Math.max(realTotal || estTotal || 0, fileMap[file].dup);
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
const files = Object.entries(fileMap)
|
|
60
60
|
.map(([file, { dup, total = 0 }]) => {
|
|
61
61
|
const percentage = total > 0 ? (dup / total) * 100 : 0;
|
|
62
62
|
const stats = {
|
|
@@ -84,6 +84,7 @@ export function extractWorstFiles(report) {
|
|
|
84
84
|
return a[0].localeCompare(b[0]);
|
|
85
85
|
})
|
|
86
86
|
.slice(0, count);
|
|
87
|
+
return { files, fileCount: Object.keys(fileMap).length };
|
|
87
88
|
}
|
|
88
89
|
function parseAppCodeDuplicationReport(jsonPath) {
|
|
89
90
|
try {
|
|
@@ -91,14 +92,22 @@ function parseAppCodeDuplicationReport(jsonPath) {
|
|
|
91
92
|
const total = report.statistics?.total ?? {};
|
|
92
93
|
const totalLines = total.lines ?? 0;
|
|
93
94
|
const duplicateLines = total.duplicatedLines ?? 0;
|
|
95
|
+
const percentage = total.percentage ?? (totalLines > 0 ? (duplicateLines / totalLines) * 100 : 0);
|
|
96
|
+
const { files: worstFiles, fileCount } = extractWorstFiles(report);
|
|
97
|
+
const perFilePercentages = worstFiles.map(([, f]) => f.percentage);
|
|
98
|
+
const minPercentage = perFilePercentages.length ? Math.min(...perFilePercentages) : percentage;
|
|
99
|
+
const maxPercentage = perFilePercentages.length ? Math.max(...perFilePercentages) : percentage;
|
|
94
100
|
return {
|
|
95
101
|
totalLines,
|
|
96
102
|
totalTokens: total.tokens ?? 0,
|
|
97
103
|
duplicateLines,
|
|
98
104
|
duplicateTokens: total.duplicatedTokens ?? 0,
|
|
99
105
|
clonesCount: total.clones ?? 0,
|
|
100
|
-
percentage
|
|
101
|
-
|
|
106
|
+
percentage,
|
|
107
|
+
minPercentage,
|
|
108
|
+
maxPercentage,
|
|
109
|
+
worstFiles,
|
|
110
|
+
filesWithDuplication: fileCount,
|
|
102
111
|
};
|
|
103
112
|
}
|
|
104
113
|
catch (err) {
|
|
@@ -109,8 +118,11 @@ function parseAppCodeDuplicationReport(jsonPath) {
|
|
|
109
118
|
duplicateTokens: 0,
|
|
110
119
|
clonesCount: 0,
|
|
111
120
|
percentage: 0,
|
|
121
|
+
minPercentage: 0,
|
|
122
|
+
maxPercentage: 0,
|
|
112
123
|
error: err.message,
|
|
113
124
|
worstFiles: [],
|
|
125
|
+
filesWithDuplication: 0,
|
|
114
126
|
};
|
|
115
127
|
}
|
|
116
128
|
}
|
|
@@ -157,11 +169,12 @@ const getColorForDuplicationStatus = (pct) => {
|
|
|
157
169
|
return pct < green ? 'green' : pct < orange ? 'orange' : 'red';
|
|
158
170
|
};
|
|
159
171
|
function renderWorstFiles(app, worstFiles = []) {
|
|
172
|
+
const threshold = codeDuplicationConfig.worstFiles.threshold;
|
|
160
173
|
if (!worstFiles.length) {
|
|
161
174
|
return `
|
|
162
|
-
<details>
|
|
163
|
-
<summary
|
|
164
|
-
<p
|
|
175
|
+
<details class="coverage-details">
|
|
176
|
+
<summary>📊 Duplication Overview for <strong>${app}</strong></summary>
|
|
177
|
+
<p class="coverage-empty">No files above threshold 🎉</p>
|
|
165
178
|
</details>`;
|
|
166
179
|
}
|
|
167
180
|
const rows = worstFiles
|
|
@@ -169,21 +182,39 @@ function renderWorstFiles(app, worstFiles = []) {
|
|
|
169
182
|
const short = shortPathFromAppsRoot(file, app);
|
|
170
183
|
const color = getColorForDuplicationStatus(f.percentage);
|
|
171
184
|
return `
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
<tr>
|
|
186
|
+
<td colspan="2" title="${file}">${short}</td>
|
|
187
|
+
<td>${f.duplicatedLines}/${f.totalLines}</td>
|
|
188
|
+
<td style="color:${color}">${f.percentage.toFixed(2)}%</td>
|
|
189
|
+
</tr>`;
|
|
177
190
|
})
|
|
178
191
|
.join('');
|
|
179
192
|
return `
|
|
180
|
-
<details>
|
|
181
|
-
<summary
|
|
182
|
-
<table
|
|
193
|
+
<details class="coverage-details">
|
|
194
|
+
<summary>📊 Duplication Overview for <strong>${app}</strong> (≥ ${threshold}%)</summary>
|
|
195
|
+
<table class="coverage-subtable">
|
|
183
196
|
<tbody>${rows}</tbody>
|
|
184
197
|
</table>
|
|
185
198
|
</details>`;
|
|
186
199
|
}
|
|
200
|
+
function renderDuplicationCell(stats) {
|
|
201
|
+
const min = stats.minPercentage ?? stats.percentage;
|
|
202
|
+
const max = stats.maxPercentage ?? stats.percentage;
|
|
203
|
+
if (stats.duplicateLines === 0) {
|
|
204
|
+
return `<span style="color:${getColorForDuplicationStatus(0)}">0.00%</span>`;
|
|
205
|
+
}
|
|
206
|
+
if (min.toFixed(2) === max.toFixed(2)) {
|
|
207
|
+
return `
|
|
208
|
+
${stats.percentage.toFixed(2)}% (${stats.filesWithDuplication} files cluster at
|
|
209
|
+
<span style="color:${getColorForDuplicationStatus(min)}">${min.toFixed(2)}%</span>)
|
|
210
|
+
`;
|
|
211
|
+
}
|
|
212
|
+
return `
|
|
213
|
+
${stats.percentage.toFixed(2)}% (range:
|
|
214
|
+
<span style="color:${getColorForDuplicationStatus(min)}">${min.toFixed(2)}%</span> –
|
|
215
|
+
<span style="color:${getColorForDuplicationStatus(max)}">${max.toFixed(2)}%</span>)
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
187
218
|
function buildAppRow(app, stats) {
|
|
188
219
|
if (stats.error) {
|
|
189
220
|
return `<tr style="background:#ffe0e0">
|
|
@@ -191,15 +222,16 @@ function buildAppRow(app, stats) {
|
|
|
191
222
|
<td colspan="5" style="color:red">${stats.error}</td>
|
|
192
223
|
</tr>`;
|
|
193
224
|
}
|
|
194
|
-
const
|
|
195
|
-
const
|
|
225
|
+
const max = stats.maxPercentage ?? stats.percentage;
|
|
226
|
+
const rowColor = getColorForDuplicationStatus(max);
|
|
227
|
+
const bg = rowColor === 'green' ? '#e6ffed' : rowColor === 'orange' ? '#fff8e1' : '#ffe6e6';
|
|
196
228
|
return `
|
|
197
229
|
<tr style="background:${bg}">
|
|
198
230
|
<td>${app}</td>
|
|
199
231
|
<td>${stats.totalLines}</td>
|
|
200
232
|
<td>${stats.duplicateLines}</td>
|
|
201
233
|
<td>${stats.clonesCount}</td>
|
|
202
|
-
<td style="font-weight:bold;
|
|
234
|
+
<td style="font-weight:bold;">${renderDuplicationCell(stats)}</td>
|
|
203
235
|
</tr>
|
|
204
236
|
<tr><td colspan="5">${renderWorstFiles(app, stats.worstFiles)}</td></tr>`;
|
|
205
237
|
}
|
|
@@ -217,6 +249,28 @@ export function generateCodeDuplicationHtml(summary) {
|
|
|
217
249
|
table { border-collapse: collapse; width: 100%; }
|
|
218
250
|
th, td { border: 1px solid #ccc; padding: 0.5rem; text-align: left; }
|
|
219
251
|
th { background: #f0f0f0; }
|
|
252
|
+
.coverage-details {
|
|
253
|
+
margin: 0.3rem 0 0.8rem 1.5rem;
|
|
254
|
+
padding: 0.5rem;
|
|
255
|
+
border-left: 3px solid #ccc;
|
|
256
|
+
background: #fafafa;
|
|
257
|
+
border-radius: 4px;
|
|
258
|
+
}
|
|
259
|
+
.coverage-details summary {
|
|
260
|
+
font-weight: bold;
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
}
|
|
263
|
+
.coverage-subtable {
|
|
264
|
+
margin-top: 0.5rem;
|
|
265
|
+
border: 1px solid #ddd;
|
|
266
|
+
width: 95%;
|
|
267
|
+
font-size: 0.9em;
|
|
268
|
+
}
|
|
269
|
+
.coverage-empty {
|
|
270
|
+
margin: 0.5rem 0;
|
|
271
|
+
color: #888;
|
|
272
|
+
font-style: italic;
|
|
273
|
+
}
|
|
220
274
|
</style>
|
|
221
275
|
</head>
|
|
222
276
|
<body>
|
|
@@ -4,7 +4,10 @@ export interface AppCodeDuplicationSummary {
|
|
|
4
4
|
duplicateLines: number;
|
|
5
5
|
duplicateTokens: number;
|
|
6
6
|
percentage: number;
|
|
7
|
+
minPercentage: number;
|
|
8
|
+
maxPercentage: number;
|
|
7
9
|
clonesCount: number;
|
|
10
|
+
filesWithDuplication: number;
|
|
8
11
|
error?: string;
|
|
9
12
|
worstFiles?: WorstCodeDuplicationFile[];
|
|
10
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeDuplication.d.ts","sourceRoot":"","sources":["../../../../src/adapters/code-duplication/types/CodeDuplication.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,yBAAyB;IAExC,UAAU,EAAE,MAAM,CAAC;IAGnB,WAAW,EAAE,MAAM,CAAC;IAGpB,cAAc,EAAE,MAAM,CAAC;IAGvB,eAAe,EAAE,MAAM,CAAC;IAMxB,UAAU,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"CodeDuplication.d.ts","sourceRoot":"","sources":["../../../../src/adapters/code-duplication/types/CodeDuplication.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,yBAAyB;IAExC,UAAU,EAAE,MAAM,CAAC;IAGnB,WAAW,EAAE,MAAM,CAAC;IAGpB,cAAc,EAAE,MAAM,CAAC;IAGvB,eAAe,EAAE,MAAM,CAAC;IAMxB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IAMtB,WAAW,EAAE,MAAM,CAAC;IAMpB,oBAAoB,EAAE,MAAM,CAAC;IAM7B,KAAK,CAAC,EAAE,MAAM,CAAC;IAMf,UAAU,CAAC,EAAE,wBAAwB,EAAE,CAAC;CACzC;AAKD,MAAM,WAAW,8BAA8B;IAK7C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;IAGhD,UAAU,EAAE,MAAM,CAAC;IAGnB,WAAW,EAAE,MAAM,CAAC;IAGpB,mBAAmB,EAAE,MAAM,CAAC;IAG5B,oBAAoB,EAAE,MAAM,CAAC;IAM7B,UAAU,EAAE,MAAM,CAAC;CACpB;AAcD,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM;IACN;QAEE,eAAe,EAAE,MAAM,CAAC;QAGxB,UAAU,EAAE,MAAM,CAAC;QAGnB,UAAU,EAAE,MAAM,CAAC;KACpB;CACF,CAAC;AAKF,MAAM,WAAW,cAAc;IAE7B,eAAe,EAAE,MAAM,CAAC;IAGxB,UAAU,EAAE,MAAM,CAAC;IAGnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,UAAU,kBAAkB;IAE1B,IAAI,EAAE,MAAM,CAAC;IAMb,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAM7B,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5B;AAKD,UAAU,cAAc;IAKtB,KAAK,EAAE,MAAM,CAAC;IAGd,SAAS,EAAE,kBAAkB,CAAC;IAG9B,UAAU,EAAE,kBAAkB,CAAC;CAChC;AAMD,MAAM,WAAW,aAAa;IAK5B,UAAU,CAAC,EAAE;QACX,KAAK,CAAC,EAAE;YAEN,KAAK,CAAC,EAAE,MAAM,CAAC;YAGf,MAAM,CAAC,EAAE,MAAM,CAAC;YAGhB,eAAe,CAAC,EAAE,MAAM,CAAC;YAGzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;YAG1B,MAAM,CAAC,EAAE,MAAM,CAAC;YAMhB,UAAU,CAAC,EAAE,MAAM,CAAC;SACrB,CAAC;KACH,CAAC;IAMF,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;CAC/B"}
|
|
@@ -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;AAKtE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;
|
|
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;AAyCD,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"}
|
|
@@ -15,14 +15,18 @@ const buildAssetsInfos = (data) => data.map((entry) => {
|
|
|
15
15
|
const filename = entry?.filename ?? 'unknown';
|
|
16
16
|
const sizeKb = typeof entry?.parsedSize === 'number' ? entry.parsedSize / 1024 : 0;
|
|
17
17
|
let type = 'other';
|
|
18
|
-
if (/\.js$/i.test(filename))
|
|
18
|
+
if (/\.(js|mjs|cjs|ts|tsx)$/i.test(filename)) {
|
|
19
19
|
type = 'js';
|
|
20
|
-
|
|
20
|
+
}
|
|
21
|
+
else if (/\.css$/i.test(filename)) {
|
|
21
22
|
type = 'css';
|
|
22
|
-
|
|
23
|
+
}
|
|
24
|
+
else if (/\.html?$/i.test(filename)) {
|
|
23
25
|
type = 'html';
|
|
24
|
-
|
|
26
|
+
}
|
|
27
|
+
else if (/\.(png|jpe?g|gif|svg|webp)$/i.test(filename)) {
|
|
25
28
|
type = 'img';
|
|
29
|
+
}
|
|
26
30
|
return { filename, type, sizeKb };
|
|
27
31
|
});
|
|
28
32
|
export function parseAnalyzerJson(raw) {
|
|
@@ -1 +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,
|
|
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"}
|