@ovh-ux/manager-static-analysis-kit 0.7.1 → 0.8.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 +26 -0
- package/bin/cli-utils.js +56 -0
- package/bin/lint-cli.js +18 -16
- package/bin/perf-budgets-cli.js +359 -0
- package/bin/perf-budgets-tests-cli.js +74 -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/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 +192 -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/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/file-globs-config.d.ts +1 -1
- package/dist/configs/file-globs-config.d.ts.map +1 -1
- package/dist/configs/file-globs-config.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [0.8.0](https://github.com/ovh/manager/compare/@ovh-ux/manager-static-analysis-kit@0.7.1...@ovh-ux/manager-static-analysis-kit@0.8.0) (2025-09-11)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* adjust folder naming conventions rules ([fc7f608](https://github.com/ovh/manager/commit/fc7f6083038831902657e58f24792c7847f42d9c)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
12
|
+
* adjust thresholds ([bd950f1](https://github.com/ovh/manager/commit/bd950f1fb8e972475e9c0a26915470f4f2e29fd4)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
13
|
+
* ci lint issues ([c7b389e](https://github.com/ovh/manager/commit/c7b389e552cc1478998ad6dd24eb5c59328751da)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
14
|
+
* indentation issues ([f106dcf](https://github.com/ovh/manager/commit/f106dcf283eb453d97a5f575327f4bd7e8eb9ccb)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
15
|
+
* perf budgets readme doc indentation ([00660ba](https://github.com/ovh/manager/commit/00660ba9c418757dfc25dbb92be2af0a6e3dfea6)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
16
|
+
* readme doc indentation ([e54e5eb](https://github.com/ovh/manager/commit/e54e5eb4c0bda11f7dc1c9e41bde804664420939)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
17
|
+
* static kit integration status ([9a49e4f](https://github.com/ovh/manager/commit/9a49e4f992311de0ec2ac2876720dc46202ed118)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* add new cli commands ([cfe81c2](https://github.com/ovh/manager/commit/cfe81c255885165fdfd12bb9ba6e0a81fc8f4d18)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
23
|
+
* add safety check and testing scenarios cases ([04d9b74](https://github.com/ovh/manager/commit/04d9b747f84e7475eee267f5311bebcdb1a6749a)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
24
|
+
* include shared in turbo build by default ([d006428](https://github.com/ovh/manager/commit/d0064281ef1a4c844fa574169fac17d2301e976b)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
25
|
+
* integrate and configure bundle analysis ([514d424](https://github.com/ovh/manager/commit/514d4247b6b6e4b37379e64e5260636b5d1c7c4d)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
26
|
+
* remove debug features ([7d1ac89](https://github.com/ovh/manager/commit/7d1ac892639831ace9a539d156775dbeeae19579)), closes [#MANAGER-19352](https://github.com/ovh/manager/issues/MANAGER-19352)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
6
32
|
## [0.7.1](https://github.com/ovh/manager/compare/@ovh-ux/manager-static-analysis-kit@0.7.0...@ovh-ux/manager-static-analysis-kit@0.7.1) (2025-08-14)
|
|
7
33
|
|
|
8
34
|
|
package/bin/cli-utils.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Log an informational message with a standard prefix.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} msg - The message to log.
|
|
7
|
+
* @returns {void}
|
|
8
|
+
*/
|
|
9
|
+
export function logInfo(msg) {
|
|
10
|
+
console.log(`[manager-perf-budget] ${msg}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Log a warning message with a standard prefix.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} msg - The message to log.
|
|
17
|
+
* @returns {void}
|
|
18
|
+
*/
|
|
19
|
+
export function logWarn(msg) {
|
|
20
|
+
console.warn(`⚠️ [manager-perf-budget] ${msg}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Log an error message with a standard prefix.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} msg - The message to log.
|
|
27
|
+
* @returns {void}
|
|
28
|
+
*/
|
|
29
|
+
export function logError(msg) {
|
|
30
|
+
console.error(`❌ [manager-perf-budget] ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a given package.json belongs to a React app.
|
|
35
|
+
*
|
|
36
|
+
* A package is considered a React app if it declares `react`
|
|
37
|
+
* as a dependency, devDependency, or peerDependency.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} pkgPath - Absolute path to a `package.json` file.
|
|
40
|
+
* @returns {boolean} True if the package depends on React, otherwise false.
|
|
41
|
+
*/
|
|
42
|
+
export function isReactApp(pkgPath) {
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
45
|
+
|
|
46
|
+
/** @type {Record<string, any>} */
|
|
47
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
48
|
+
if (!pkg || typeof pkg !== 'object') return false;
|
|
49
|
+
|
|
50
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
|
|
51
|
+
return Boolean(deps.react);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logWarn(`Failed to read or parse ${pkgPath}: ${err.message}`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/bin/lint-cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'child_process';
|
|
3
|
-
import {
|
|
4
|
-
import { dirname,
|
|
5
|
-
import { cwd } from 'process';
|
|
6
|
-
import {
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import process, { cwd } from 'node:process';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { logError, logInfo } from './cli-utils.js';
|
|
7
9
|
|
|
8
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
9
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const userCwd = cwd();
|
|
11
12
|
const eslintBin = resolve(__dirname, '../node_modules/.bin/eslint');
|
|
@@ -13,11 +14,13 @@ const eslintBin = resolve(__dirname, '../node_modules/.bin/eslint');
|
|
|
13
14
|
const args = process.argv.slice(2);
|
|
14
15
|
const hasExplicitConfigArg = args.includes('--config');
|
|
15
16
|
const localConfigPath = join(userCwd, 'eslint.config.ts');
|
|
16
|
-
const sharedDefaultPath = resolve(
|
|
17
|
+
const sharedDefaultPath = resolve(
|
|
18
|
+
__dirname,
|
|
19
|
+
'../dist/adapters/eslint/config/eslint-shared-config.js',
|
|
20
|
+
);
|
|
17
21
|
const hasLocalConfig = existsSync(localConfigPath);
|
|
18
22
|
|
|
19
23
|
// Determine effective config path
|
|
20
|
-
// eslint-disable-next-line no-nested-ternary
|
|
21
24
|
const configPath = hasExplicitConfigArg
|
|
22
25
|
? args[args.indexOf('--config') + 1]
|
|
23
26
|
: hasLocalConfig
|
|
@@ -34,8 +37,8 @@ function runLint(command, args, cwd) {
|
|
|
34
37
|
});
|
|
35
38
|
|
|
36
39
|
child.on('error', (err) => {
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
logError('[manager-lint] Failed to run ESLint');
|
|
41
|
+
logError(err);
|
|
39
42
|
reject(1);
|
|
40
43
|
});
|
|
41
44
|
|
|
@@ -52,20 +55,19 @@ function runLint(command, args, cwd) {
|
|
|
52
55
|
try {
|
|
53
56
|
const mod = await import(url);
|
|
54
57
|
const configContent = mod.default ?? mod;
|
|
55
|
-
|
|
58
|
+
logInfo('[manager-lint] Loaded ESLint config from:', configPath);
|
|
56
59
|
console.dir(configContent, { depth: null });
|
|
57
60
|
} catch (err) {
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
logError(`[manager-lint] Failed to load config at ${configPath}`);
|
|
62
|
+
logError(err);
|
|
60
63
|
process.exit(1);
|
|
61
64
|
}
|
|
62
65
|
process.exit(0);
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
// Construct full args
|
|
66
|
-
const fullArgs =
|
|
67
|
-
? args
|
|
68
|
-
: ['--config', sharedDefaultPath, ...args];
|
|
69
|
+
const fullArgs =
|
|
70
|
+
hasExplicitConfigArg || hasLocalConfig ? args : ['--config', sharedDefaultPath, ...args];
|
|
69
71
|
|
|
70
72
|
// Run ESLint using tsx and wait for completion
|
|
71
73
|
const exitCode = await runLint('tsx', [eslintBin, ...fullArgs], userCwd);
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
collectPerfBudgets,
|
|
10
|
+
generatePerfBudgetsHtml,
|
|
11
|
+
} from '../dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.js';
|
|
12
|
+
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
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run vite-bundle-analyzer for given mode (html or json).
|
|
35
|
+
*/
|
|
36
|
+
function runBundleAnalyzer(appDirectory, appShortName, mode) {
|
|
37
|
+
const reportFile = `${bundleAnalysisConfig?.reportFile || 'bundle-report'}${
|
|
38
|
+
mode === 'json' ? '.json' : '.html'
|
|
39
|
+
}`;
|
|
40
|
+
const outputDir = path.join(outputRootDir, perfBudgetsReportDirName, appShortName);
|
|
41
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
logInfo(`Running bundle analyzer (${mode}) for ${appShortName} → ${outputDir}`);
|
|
44
|
+
|
|
45
|
+
const analyzerArgs = [
|
|
46
|
+
'--mode',
|
|
47
|
+
mode,
|
|
48
|
+
'--no-open',
|
|
49
|
+
'--filename',
|
|
50
|
+
bundleAnalysisConfig?.reportFile || 'bundle-report',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const result = spawnSync(bundleAnalyzerBin, analyzerArgs, {
|
|
54
|
+
cwd: appDirectory,
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
shell: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.status !== 0) {
|
|
60
|
+
logError(`❌ Analyzer (${mode}) failed for ${appShortName}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const distReport = path.join(appDirectory, 'dist', reportFile);
|
|
65
|
+
const targetReport = path.join(outputDir, reportFile);
|
|
66
|
+
|
|
67
|
+
if (fs.existsSync(distReport)) {
|
|
68
|
+
fs.copyFileSync(distReport, targetReport);
|
|
69
|
+
logInfo(`✅ ${mode.toUpperCase()} report copied to ${targetReport}`);
|
|
70
|
+
return targetReport;
|
|
71
|
+
} else {
|
|
72
|
+
logWarn(`⚠️ Expected ${mode} report not found at ${distReport}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
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.
|
|
271
|
+
*
|
|
272
|
+
* @returns {{appFolders: string[], packageNames: string[]}}
|
|
273
|
+
* An object containing discovered React app folders and an empty packageNames array.
|
|
274
|
+
*/
|
|
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: [] };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Parse CLI arguments to resolve which apps or packages should be analyzed.
|
|
292
|
+
*
|
|
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.
|
|
329
|
+
*/
|
|
330
|
+
function main() {
|
|
331
|
+
try {
|
|
332
|
+
if (!fs.existsSync(bundleAnalyzerBin)) {
|
|
333
|
+
logError(`❌ Analyzer binary not found at ${bundleAnalyzerBin}`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const { appFolders, packageNames } = parseCliTargets();
|
|
338
|
+
const turboFilters = resolveTurboFilters({ appFolders, packageNames });
|
|
339
|
+
|
|
340
|
+
// 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
|
+
}
|
|
353
|
+
} catch (err) {
|
|
354
|
+
logError(`Fatal error: ${err.stack || err.message}`);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
main();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
import { logError, logInfo } from './cli-utils.js';
|
|
6
|
+
|
|
7
|
+
// Define the set of validation test cases
|
|
8
|
+
const cases = [
|
|
9
|
+
// This case take too much time since it builds all apps, use it only for debug
|
|
10
|
+
// { name: 'Auto-discovery (all apps)', cmd: 'yarn manager-perf-budgets', expect: 0 },
|
|
11
|
+
{ name: 'Single app (zimbra)', cmd: 'yarn manager-perf-budgets --app zimbra', expect: 0 },
|
|
12
|
+
{
|
|
13
|
+
name: 'Multiple apps (container,zimbra)',
|
|
14
|
+
cmd: 'yarn manager-perf-budgets --apps container,zimbra',
|
|
15
|
+
expect: 0,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'Single package (container)',
|
|
19
|
+
cmd: 'yarn manager-perf-budgets --package @ovh-ux/manager-container-app',
|
|
20
|
+
expect: 0,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'Multiple packages (zimbra,pci-workflow)',
|
|
24
|
+
cmd: 'yarn manager-perf-budgets --packages @ovh-ux/manager-zimbra-app,@ovh-ux/manager-pci-workflow-app',
|
|
25
|
+
expect: 0,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'Mixed valid+invalid apps',
|
|
29
|
+
cmd: 'yarn manager-perf-budgets --apps zimbra,unknown-app,container',
|
|
30
|
+
expect: 0,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Mixed valid+invalid packages',
|
|
34
|
+
cmd: 'yarn manager-perf-budgets --packages @ovh-ux/manager-zimbra-app,@ovh-ux/manager-ghost-app',
|
|
35
|
+
expect: 0,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'All invalid apps',
|
|
39
|
+
cmd: 'yarn manager-perf-budgets --apps doesnotexist1,doesnotexist2',
|
|
40
|
+
expect: 1,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'All invalid packages',
|
|
44
|
+
cmd: 'yarn manager-perf-budgets --packages @ovh-ux/manager-ghost-app,@ovh-ux/manager-missing-app',
|
|
45
|
+
expect: 1,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Turbo build failure simulation (pci-ai-tools)',
|
|
49
|
+
cmd: 'yarn manager-perf-budgets --app pci-ai-tools',
|
|
50
|
+
expect: 0,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
let failed = 0;
|
|
55
|
+
|
|
56
|
+
for (const test of cases) {
|
|
57
|
+
logInfo(`\n🔎 Running: ${test.name}`);
|
|
58
|
+
const [bin, ...args] = test.cmd.split(' ');
|
|
59
|
+
const result = spawnSync(bin, args, { stdio: 'inherit', shell: true });
|
|
60
|
+
|
|
61
|
+
if (result.status !== test.expect) {
|
|
62
|
+
logError(`❌ ${test.name} failed (expected exit ${test.expect}, got ${result.status})`);
|
|
63
|
+
failed++;
|
|
64
|
+
} else {
|
|
65
|
+
logInfo(`✅ ${test.name} passed`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (failed > 0) {
|
|
70
|
+
logError(`\n❌ ${failed} test(s) failed.`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
} else {
|
|
73
|
+
logInfo('\n🎉 All perf-budgets CLI validation tests passed!');
|
|
74
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eslint-naming-conventions.d.ts","sourceRoot":"","sources":["../../../../src/adapters/eslint/rules/eslint-naming-conventions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAUhC,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,UAAU,
|
|
1
|
+
{"version":3,"file":"eslint-naming-conventions.d.ts","sourceRoot":"","sources":["../../../../src/adapters/eslint/rules/eslint-naming-conventions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAUhC,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,UAAU,EA2CpD,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PerfBudgetsSummary } from '../types/PerfBudget.js';
|
|
2
|
+
import { ParsedAsset, ParsedBundle } from '../types/PerfBudgetAssets.js';
|
|
3
|
+
export declare function escapeHtml(str: string): string;
|
|
4
|
+
export declare function parseAnalyzerJson(raw: ParsedAsset[]): ParsedBundle;
|
|
5
|
+
export declare function collectPerfBudgets(rootDir: string): PerfBudgetsSummary;
|
|
6
|
+
export declare function generatePerfBudgetsHtml(summary: PerfBudgetsSummary): string;
|
|
7
|
+
//# sourceMappingURL=perfs-bundle-analysis-helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perfs-bundle-analysis-helper.d.ts","sourceRoot":"","sources":["../../../../src/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.ts"],"names":[],"mappings":"AAMA,OAAO,EAAoB,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAOtE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAmCD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,YAAY,CAgBlE;AASD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAmCtE;AASD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAwJ3E"}
|