@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +166 -24
  3. package/bin/cli-path-config.js +41 -2
  4. package/bin/code-duplication-cli.js +14 -16
  5. package/bin/code-duplication-tests-cli.js +43 -6
  6. package/bin/perf-budgets-cli.js +15 -15
  7. package/bin/perf-budgets-tests-cli.js +36 -3
  8. package/bin/static-dynamic-quality-checks.js +2 -2
  9. package/bin/tests-coverage-cli-tests.js +44 -6
  10. package/bin/tests-coverage-cli.js +35 -25
  11. package/bin/types-coverage-cli.js +76 -11
  12. package/bin/types-coverage-tests-cli.js +44 -7
  13. package/bin/utils/args-parse-utils.js +77 -107
  14. package/bin/utils/module-utils.js +121 -0
  15. package/bin/utils/runner-utils.js +117 -22
  16. package/bin/utils/turbo-utils.js +15 -74
  17. package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.d.ts +4 -1
  18. package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.d.ts.map +1 -1
  19. package/dist/adapters/code-duplication/helpers/code-duplication-analysis-helper.js +72 -18
  20. package/dist/adapters/code-duplication/types/CodeDuplication.d.ts +3 -0
  21. package/dist/adapters/code-duplication/types/CodeDuplication.d.ts.map +1 -1
  22. package/dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.d.ts.map +1 -1
  23. package/dist/adapters/perf-budgets/helpers/perfs-bundle-analysis-helper.js +8 -4
  24. package/dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.d.ts.map +1 -1
  25. package/dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.js +31 -8
  26. package/dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.d.ts.map +1 -1
  27. package/dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.js +121 -48
  28. package/dist/adapters/types-coverage/types/TypesCoverage.d.ts +8 -0
  29. package/dist/adapters/types-coverage/types/TypesCoverage.d.ts.map +1 -1
  30. package/dist/configs/code-duplication-config.d.ts +1 -0
  31. package/dist/configs/code-duplication-config.d.ts.map +1 -1
  32. package/dist/configs/code-duplication-config.js +2 -1
  33. package/dist/configs/shared-exclusion-patterns.d.ts +1 -0
  34. package/dist/configs/shared-exclusion-patterns.d.ts.map +1 -1
  35. package/dist/configs/shared-exclusion-patterns.js +21 -0
  36. package/dist/configs/tests-coverage-config.js +1 -1
  37. package/dist/configs/types-coverage-config.d.ts +14 -0
  38. package/dist/configs/types-coverage-config.d.ts.map +1 -1
  39. package/dist/configs/types-coverage-config.js +13 -16
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +1 -1
  42. package/bin/utils/apps-utils.js +0 -64
@@ -3,14 +3,12 @@ import { spawnSync } from 'node:child_process';
3
3
 
4
4
  import { logError, logInfo } from './utils/log-utils.js';
5
5
 
6
+ // Define the set of validation test cases
6
7
  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
8
  { name: 'Single app (zimbra)', cmd: 'yarn manager-tests-coverage --app zimbra', expect: 0 },
11
9
  {
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',
10
+ name: 'Multiple apps (container,zimbra)',
11
+ cmd: 'yarn manager-tests-coverage --apps container,zimbra',
14
12
  expect: 0,
15
13
  },
16
14
  {
@@ -23,6 +21,36 @@ const cases = [
23
21
  cmd: 'yarn manager-tests-coverage --packages @ovh-ux/manager-zimbra-app,@ovh-ux/manager-pci-workflow-app',
24
22
  expect: 0,
25
23
  },
24
+ {
25
+ name: 'Single library static path by folder name (manager-react-components)',
26
+ cmd: 'yarn manager-tests-coverage --library manager-react-components',
27
+ expect: 0,
28
+ },
29
+ {
30
+ name: 'Single library static path by package name (manager-react-components)',
31
+ cmd: 'yarn manager-tests-coverage --library @ovh-ux/manager-react-components',
32
+ expect: 0,
33
+ },
34
+ {
35
+ name: 'Single library dynamic path by folder name (shell-client)',
36
+ cmd: 'yarn manager-tests-coverage --library shell-client',
37
+ expect: 0,
38
+ },
39
+ {
40
+ name: 'Single library static path by package name (shell-client)',
41
+ cmd: 'yarn manager-tests-coverage --library @ovh-ux/manager-react-shell-client',
42
+ expect: 0,
43
+ },
44
+ {
45
+ name: 'Multiple libraries by folder names (manager-wiki,manager-react-components,shell-client,logs-to-customer)',
46
+ cmd: 'yarn manager-tests-coverage --libraries manager-wiki,manager-react-components,shell-client,logs-to-customer',
47
+ expect: 0,
48
+ },
49
+ {
50
+ name: 'Multiple libraries by package names (manager-wiki,manager-react-components,shell-client,logs-to-customer)',
51
+ cmd: 'yarn manager-tests-coverage --libraries @ovh-ux/manager-wiki,@ovh-ux/manager-react-components,@ovh-ux/manager-react-shell-client,@ovh-ux/logs-to-customer',
52
+ expect: 0,
53
+ },
26
54
  {
27
55
  name: 'Mixed valid+invalid apps',
28
56
  cmd: 'yarn manager-tests-coverage --apps zimbra,unknown-app,container',
@@ -30,7 +58,12 @@ const cases = [
30
58
  },
31
59
  {
32
60
  name: 'Mixed valid+invalid packages',
33
- cmd: 'yarn manager-tests-coverage --packages @ovh-ux/manager-zimbra-app,@ovh-ux/manager-ghost-app',
61
+ cmd: 'yarn manager-tests-coverage --packages @ovh-ux/manager-wiki,manager-react-components,@ovh-ux/manager-ghost-app',
62
+ expect: 0,
63
+ },
64
+ {
65
+ name: 'Mixed valid+invalid libraries',
66
+ cmd: 'yarn manager-tests-coverage --libraries @ovh-ux/manager-zimbra-app,@ovh-ux/manager-ghost-app',
34
67
  expect: 0,
35
68
  },
36
69
  {
@@ -43,6 +76,11 @@ const cases = [
43
76
  cmd: 'yarn manager-tests-coverage --packages @ovh-ux/manager-ghost-app,@ovh-ux/manager-missing-app',
44
77
  expect: 1,
45
78
  },
79
+ {
80
+ name: 'Turbo build failure simulation (pci-ai-tools)',
81
+ cmd: 'yarn manager-tests-coverage --app pci-ai-tools',
82
+ expect: 0,
83
+ },
46
84
  ];
47
85
 
48
86
  let failed = 0;
@@ -8,17 +8,16 @@ import {
8
8
  generateTestsCoverageHtml,
9
9
  } from '../dist/adapters/tests-coverage/helpers/tests-coverage-analysis-helper.js';
10
10
  import {
11
- appsDir,
12
- outputRootDir,
13
11
  rootDir,
14
12
  testsCoverageCombinedHtmlReportName,
15
13
  testsCoverageCombinedJsonReportName,
14
+ testsCoverageOutputRootDir,
16
15
  testsCoverageReportsRootDirName,
17
16
  } from './cli-path-config.js';
18
17
  import { parseCliTargets } from './utils/args-parse-utils.js';
19
18
  import { logError, logInfo, logWarn } from './utils/log-utils.js';
20
- import { runAppsAnalysis } from './utils/runner-utils.js';
21
- import { resolveTurboFilters, runTurboBuild, runTurboTests } from './utils/turbo-utils.js';
19
+ import { runModulesAnalysis } from './utils/runner-utils.js';
20
+ import { runTurboBuild, runTurboTests } from './utils/turbo-utils.js';
22
21
 
23
22
  /**
24
23
  * Locate common coverage artifacts inside an app directory.
@@ -63,7 +62,11 @@ function findCoverageFiles(appDir) {
63
62
  */
64
63
  function harvestAppCoverage(appDir, appShortName) {
65
64
  const { summary, lcov } = findCoverageFiles(appDir);
66
- const outDir = path.join(outputRootDir, testsCoverageReportsRootDirName, appShortName);
65
+ const outDir = path.join(
66
+ testsCoverageOutputRootDir,
67
+ testsCoverageReportsRootDirName,
68
+ appShortName,
69
+ );
67
70
  fs.mkdirSync(outDir, { recursive: true });
68
71
 
69
72
  if (!summary) {
@@ -89,32 +92,39 @@ function harvestAppCoverage(appDir, appShortName) {
89
92
  */
90
93
  function main() {
91
94
  try {
92
- const { appFolders, packageNames } = parseCliTargets(appsDir);
93
- const turboFilters = resolveTurboFilters({ appsDir, appFolders, packageNames });
95
+ const modules = parseCliTargets();
94
96
 
95
97
  // 1) Build all selected targets (best-effort)
96
- runTurboBuild(rootDir, turboFilters);
98
+ runTurboBuild(
99
+ rootDir,
100
+ modules.map((item) => item.packageName),
101
+ ['--continue'],
102
+ );
97
103
 
98
104
  // 2) Run tests with coverage; keep args minimal for Vitest/Jest compatibility
99
105
  // (Projects should already be configured to emit json-summary/lcov.)
100
- runTurboTests(rootDir, turboFilters, [
101
- '--',
102
- '--coverage',
103
- '--coverage.reporter',
104
- 'json-summary',
105
- '--coverage.reporter',
106
- 'lcov',
107
- '--coverage.reporter',
108
- 'html',
109
- ]);
106
+ runTurboTests(
107
+ rootDir,
108
+ modules.map((item) => item.packageName),
109
+ [
110
+ '--continue', // turbo: keep going even if some tests fail
111
+ '--', // pass the rest to the test runner
112
+ '--coverage',
113
+ '--coverage.reportOnFailure',
114
+ '--coverage.reporter',
115
+ 'json-summary',
116
+ '--coverage.reporter',
117
+ 'lcov',
118
+ '--coverage.reporter',
119
+ 'html',
120
+ ],
121
+ );
110
122
 
111
123
  // 3) Per-app harvest + combined reports
112
- runAppsAnalysis({
113
- appsDir,
114
- appFolders,
115
- requireReact: false,
124
+ runModulesAnalysis({
125
+ modules,
116
126
  binaryLabel: 'Tests coverage',
117
- appRunner: harvestAppCoverage,
127
+ analysisRunner: harvestAppCoverage,
118
128
  reportsRootDirName: testsCoverageReportsRootDirName,
119
129
  combinedJson: testsCoverageCombinedJsonReportName,
120
130
  combinedHtml: testsCoverageCombinedHtmlReportName,
@@ -122,9 +132,9 @@ function main() {
122
132
  generateHtmlFn: (combined) =>
123
133
  generateTestsCoverageHtml(
124
134
  combined,
125
- path.join(outputRootDir, testsCoverageReportsRootDirName),
135
+ path.join(testsCoverageOutputRootDir, testsCoverageReportsRootDirName),
126
136
  ),
127
- outputRootDir,
137
+ outputRootDir: testsCoverageOutputRootDir,
128
138
  });
129
139
  } catch (err) {
130
140
  logError(`Fatal error: ${err.stack || err.message}`);
@@ -2,22 +2,22 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import process from 'node:process';
5
+ import ts from 'typescript';
5
6
 
6
7
  import {
7
8
  collectTypesCoverage,
8
9
  generateTypesCoverageHtml,
9
10
  } from '../dist/adapters/types-coverage/helpers/types-coverage-analysis-helper.js';
10
11
  import {
11
- appsDir,
12
- outputRootDir,
13
12
  tsTypesCoverageBin,
14
13
  typesCoverageCombinedHtmlReportName,
15
14
  typesCoverageCombinedJsonReportName,
15
+ typesCoverageOutputRootDir,
16
16
  typesCoverageReportsRootDirName,
17
17
  } from './cli-path-config.js';
18
18
  import { buildTypesCoverageArgs, parseCliTargets } from './utils/args-parse-utils.js';
19
19
  import { logError, logInfo, logWarn } from './utils/log-utils.js';
20
- import { ensureBinExists, runAppsAnalysis, runCommand } from './utils/runner-utils.js';
20
+ import { ensureBinExists, runCommand, runModulesAnalysis } from './utils/runner-utils.js';
21
21
 
22
22
  /**
23
23
  * Temporarily strip "extends" from tsconfig.json before analysis,
@@ -60,6 +60,66 @@ function patchTsConfig(appDir, fn) {
60
60
  }
61
61
  }
62
62
 
63
+ /**
64
+ * Count loose TypeScript constructs (`as`, `any`, `unknown`) in src/.
65
+ *
66
+ * Uses the TypeScript compiler API to avoid false positives from strings, comments, etc.
67
+ *
68
+ * @param {string} appDir Absolute path to app root
69
+ * @returns {{ as: number; any: number; unknown: number }}
70
+ */
71
+ export function countLooseTypes(appDir) {
72
+ const srcDir = path.join(appDir, 'src');
73
+ let asCount = 0;
74
+ let anyCount = 0;
75
+ let unknownCount = 0;
76
+
77
+ function analyzeFile(filePath) {
78
+ const content = fs.readFileSync(filePath, 'utf8');
79
+ const source = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
80
+
81
+ function visit(node) {
82
+ // "as" expressions: foo as string
83
+ if (ts.isAsExpression(node)) {
84
+ asCount++;
85
+ if (node.type.kind === ts.SyntaxKind.AnyKeyword) {
86
+ anyCount++;
87
+ } else if (node.type.kind === ts.SyntaxKind.UnknownKeyword) {
88
+ unknownCount++;
89
+ }
90
+ }
91
+
92
+ // Type annotations like let x: any or function foo(x: unknown)
93
+ if (node.kind === ts.SyntaxKind.AnyKeyword) {
94
+ anyCount++;
95
+ } else if (node.kind === ts.SyntaxKind.UnknownKeyword) {
96
+ unknownCount++;
97
+ }
98
+
99
+ ts.forEachChild(node, visit);
100
+ }
101
+
102
+ visit(source);
103
+ }
104
+
105
+ function walk(dir) {
106
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
107
+ const fullPath = path.join(dir, entry.name);
108
+ if (entry.isDirectory()) {
109
+ walk(fullPath);
110
+ } else if (/\.(ts|tsx)$/.test(entry.name)) {
111
+ analyzeFile(fullPath);
112
+ }
113
+ }
114
+ }
115
+
116
+ if (fs.existsSync(srcDir)) {
117
+ walk(srcDir);
118
+ }
119
+
120
+ return { as: asCount, any: anyCount, unknown: unknownCount };
121
+ }
122
+
63
123
  /**
64
124
  * Run TypeScript coverage for one app.
65
125
  *
@@ -70,7 +130,7 @@ function patchTsConfig(appDir, fn) {
70
130
  function runAppTypesCoverage(appDir, appShortName) {
71
131
  return patchTsConfig(appDir, () => {
72
132
  const absoluteOutputDir = path.join(
73
- outputRootDir,
133
+ typesCoverageOutputRootDir,
74
134
  typesCoverageReportsRootDirName,
75
135
  appShortName,
76
136
  );
@@ -82,6 +142,10 @@ function runAppTypesCoverage(appDir, appShortName) {
82
142
  const args = buildTypesCoverageArgs(relativeOutputDir);
83
143
  const ok = runCommand(tsTypesCoverageBin, args, appDir);
84
144
 
145
+ const looseTypes = countLooseTypes(appDir);
146
+ const looseTypeReport = path.join(absoluteOutputDir, 'loose-types.json');
147
+ fs.writeFileSync(looseTypeReport, JSON.stringify(looseTypes, null, 2));
148
+
85
149
  const jsonReport = path.join(absoluteOutputDir, 'typescript-coverage.json');
86
150
  const htmlReport = path.join(absoluteOutputDir, 'index.html');
87
151
 
@@ -91,6 +155,9 @@ function runAppTypesCoverage(appDir, appShortName) {
91
155
  if (!fs.existsSync(htmlReport)) {
92
156
  logWarn(`⚠️ Missing index.html at ${htmlReport}`);
93
157
  }
158
+ if (!fs.existsSync(looseTypeReport)) {
159
+ logWarn(`⚠️ Missing loose-types.json at ${looseTypeReport}`);
160
+ }
94
161
 
95
162
  return ok;
96
163
  });
@@ -103,20 +170,18 @@ function main() {
103
170
  try {
104
171
  ensureBinExists(tsTypesCoverageBin, 'typescript-coverage-report');
105
172
 
106
- const { appFolders } = parseCliTargets(appsDir);
173
+ const modules = parseCliTargets();
107
174
 
108
- runAppsAnalysis({
109
- appsDir,
110
- appFolders,
111
- requireReact: true,
175
+ runModulesAnalysis({
176
+ modules,
112
177
  binaryLabel: 'Type coverage',
113
- appRunner: runAppTypesCoverage,
178
+ analysisRunner: runAppTypesCoverage,
114
179
  reportsRootDirName: typesCoverageReportsRootDirName,
115
180
  combinedJson: typesCoverageCombinedJsonReportName,
116
181
  combinedHtml: typesCoverageCombinedHtmlReportName,
117
182
  collectFn: collectTypesCoverage,
118
183
  generateHtmlFn: generateTypesCoverageHtml,
119
- outputRootDir,
184
+ outputRootDir: typesCoverageOutputRootDir,
120
185
  });
121
186
  } catch (err) {
122
187
  logError(`Fatal error: ${err.stack || err.message}`);
@@ -3,15 +3,12 @@ import { spawnSync } from 'node:child_process';
3
3
 
4
4
  import { logError, logInfo } from './utils/log-utils.js';
5
5
 
6
- // Define the set of validation test cases for types-coverage-tests CLI
6
+ // Define the set of validation test cases
7
7
  const cases = [
8
- // Debug only — very slow, runs across all apps
9
- // { name: 'Auto-discovery (all apps)', cmd: 'yarn manager-types-coverage', expect: 0 },
10
-
11
8
  { name: 'Single app (zimbra)', cmd: 'yarn manager-types-coverage --app zimbra', expect: 0 },
12
9
  {
13
- name: 'Multiple apps (container,zimbra,web-ongoing-operations,key-management-service)',
14
- cmd: 'yarn manager-types-coverage --apps container,zimbra,web-ongoing-operations,key-management-service',
10
+ name: 'Multiple apps (container,zimbra)',
11
+ cmd: 'yarn manager-types-coverage --apps container,zimbra',
15
12
  expect: 0,
16
13
  },
17
14
  {
@@ -24,6 +21,36 @@ const cases = [
24
21
  cmd: 'yarn manager-types-coverage --packages @ovh-ux/manager-zimbra-app,@ovh-ux/manager-pci-workflow-app',
25
22
  expect: 0,
26
23
  },
24
+ {
25
+ name: 'Single library static path by folder name (manager-react-components)',
26
+ cmd: 'yarn manager-types-coverage --library manager-react-components',
27
+ expect: 0,
28
+ },
29
+ {
30
+ name: 'Single library static path by package name (manager-react-components)',
31
+ cmd: 'yarn manager-types-coverage --library @ovh-ux/manager-react-components',
32
+ expect: 0,
33
+ },
34
+ {
35
+ name: 'Single library dynamic path by folder name (shell-client)',
36
+ cmd: 'yarn manager-types-coverage --library shell-client',
37
+ expect: 0,
38
+ },
39
+ {
40
+ name: 'Single library static path by package name (shell-client)',
41
+ cmd: 'yarn manager-types-coverage --library @ovh-ux/manager-react-shell-client',
42
+ expect: 0,
43
+ },
44
+ {
45
+ name: 'Multiple libraries by folder names (manager-wiki,manager-react-components,shell-client,logs-to-customer)',
46
+ cmd: 'yarn manager-types-coverage --libraries manager-wiki,manager-react-components,shell-client,logs-to-customer',
47
+ expect: 0,
48
+ },
49
+ {
50
+ name: 'Multiple libraries by package names (manager-wiki,manager-react-components,shell-client,logs-to-customer)',
51
+ cmd: 'yarn manager-types-coverage --libraries @ovh-ux/manager-wiki,@ovh-ux/manager-react-components,@ovh-ux/manager-react-shell-client,@ovh-ux/logs-to-customer',
52
+ expect: 0,
53
+ },
27
54
  {
28
55
  name: 'Mixed valid+invalid apps',
29
56
  cmd: 'yarn manager-types-coverage --apps zimbra,unknown-app,container',
@@ -31,7 +58,12 @@ const cases = [
31
58
  },
32
59
  {
33
60
  name: 'Mixed valid+invalid packages',
34
- cmd: 'yarn manager-types-coverage --packages @ovh-ux/manager-zimbra-app,@ovh-ux/manager-ghost-app',
61
+ cmd: 'yarn manager-types-coverage --packages @ovh-ux/manager-wiki,manager-react-components,@ovh-ux/manager-ghost-app',
62
+ expect: 0,
63
+ },
64
+ {
65
+ name: 'Mixed valid+invalid libraries',
66
+ cmd: 'yarn manager-types-coverage --libraries @ovh-ux/manager-zimbra-app,@ovh-ux/manager-ghost-app',
35
67
  expect: 0,
36
68
  },
37
69
  {
@@ -44,6 +76,11 @@ const cases = [
44
76
  cmd: 'yarn manager-types-coverage --packages @ovh-ux/manager-ghost-app,@ovh-ux/manager-missing-app',
45
77
  expect: 1,
46
78
  },
79
+ {
80
+ name: 'Turbo build failure simulation (pci-ai-tools)',
81
+ cmd: 'yarn manager-types-coverage --app pci-ai-tools',
82
+ expect: 0,
83
+ },
47
84
  ];
48
85
 
49
86
  let failed = 0;
@@ -1,143 +1,106 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
1
  import process from 'node:process';
4
2
 
5
3
  import { codeDuplicationConfig } from '../../dist/configs/code-duplication-config.js';
6
4
  import { typesCoverageConfig } from '../../dist/configs/types-coverage-config.js';
7
- import { isReactApp } from './apps-utils.js';
8
- import { logError, logInfo, logWarn } from './log-utils.js';
5
+ import { logError, logInfo } from './log-utils.js';
6
+ import { getModule } from './module-utils.js';
9
7
 
10
8
  /**
11
- * Extract an app folder name from an OVH Manager package name.
9
+ * Parse CLI arguments to resolve target modules (apps, packages, or libraries).
12
10
  *
13
- * Example:
14
- * "@ovh-ux/manager-zimbra-app" → "zimbra"
11
+ * This function looks for one of the supported flags in `process.argv`:
12
+ * - `--apps` or `--app`
13
+ * - `--packages` or `--package`
14
+ * - `--libraries` or `--library`
15
15
  *
16
- * @param {string} packageName - Full npm package name.
17
- * @returns {string|null} The extracted app folder name, or null if the format is invalid.
18
- */
19
- function extractAppFolderFromPackageName(packageName) {
20
- const match = packageName.match(/^@ovh-ux\/manager-(.+)-app$/);
21
- if (!match) {
22
- logError(`❌ Invalid package name format: ${packageName}`);
23
- return null;
24
- }
25
- return match[1];
26
- }
27
-
28
- /**
29
- * Parse CLI arguments for `--apps` or `--app` flags.
16
+ * Each flag must be followed by a comma-separated list of module identifiers.
17
+ * Each identifier is resolved using {@link getModule}, which returns a module descriptor.
30
18
  *
31
- * Accepts a comma-separated list of app folder names. Example:
32
- * `--apps zimbra,web`
19
+ * For every valid pattern:
20
+ * - The corresponding module descriptor (containing `fullPath`, `shortPath`, and `packageName`)
21
+ * is added to the result list.
22
+ * - If a module cannot be resolved, an error is logged and it is skipped.
33
23
  *
34
- * @param {string} appsDir - Absolute path to the apps root directory.
35
- * @returns {{appFolders: string[], packageNames: string[]} | null}
36
- * Object containing valid app folders and an empty `packageNames` array,
37
- * or null if no `--apps`/`--app` flag is provided.
38
- */
39
- function parseAppsArg(appsDir) {
40
- const appsArg = process.argv.find((arg) => arg === '--apps' || arg === '--app');
41
- if (!appsArg) return null;
42
-
43
- const index = process.argv.indexOf(appsArg);
44
- const rawValue = process.argv[index + 1] || '';
45
- const appFolders = rawValue
46
- .split(',')
47
- .map((n) => n.trim())
48
- .filter(Boolean);
49
-
50
- const validFolders = appFolders.filter((folder) => {
51
- if (!fs.existsSync(path.join(appsDir, folder))) {
52
- logError(`❌ App not found: ${folder} → skipping`);
53
- return false;
54
- }
55
- return true;
56
- });
57
-
58
- logInfo(`Running in apps mode for: ${validFolders.join(', ')}`);
59
- return { appFolders: validFolders, packageNames: [] };
60
- }
61
-
62
- /**
63
- * Parse CLI arguments for `--packages` or `--package` flags.
24
+ * @typedef {Object} ModuleDescriptor
25
+ * @property {string} fullPath - Absolute path to the module directory.
26
+ * @property {string} shortPath - Folder name (short identifier) of the module.
27
+ * @property {string} packageName - Full npm package name (e.g. "@ovh-ux/manager-core-shell").
64
28
  *
65
- * Accepts a comma-separated list of package names. Example:
66
- * `--packages @ovh-ux/manager-zimbra-app,@ovh-ux/manager-web-app`
29
+ * @returns {ModuleDescriptor[] | null}
30
+ * Array of resolved module descriptors, or `null` if no supported CLI flags were provided.
67
31
  *
68
- * Each package name is mapped to its corresponding app folder.
32
+ * @example
33
+ * // Example 1: Apps mode
34
+ * // CLI: --apps zimbra,web
35
+ * parseArgs();
36
+ * // → [
37
+ * // { fullPath: "/.../manager/apps/zimbra", shortPath: "zimbra", packageName: "@ovh-ux/manager-zimbra-app" },
38
+ * // { fullPath: "/.../manager/apps/web", shortPath: "web", packageName: "@ovh-ux/manager-web-app" }
39
+ * // ]
69
40
  *
70
- * @param {string} appsDir - Absolute path to the apps root directory.
71
- * @returns {{appFolders: string[], packageNames: string[]} | null}
72
- * Object mapping package names to discovered app folders,
73
- * or null if no `--packages`/`--package` flag is provided.
41
+ * @example
42
+ * // Example 2: Libraries mode
43
+ * // CLI: --libraries core-shell,react-components
44
+ * parseArgs();
45
+ * // → [
46
+ * // { fullPath: "/.../packages/manager/core/core-shell", shortPath: "core-shell", packageName: "@ovh-ux/manager-core-shell" },
47
+ * // { fullPath: "/.../packages/components/react-components", shortPath: "react-components", packageName: "@ovh-ux/manager-react-components" }
48
+ * // ]
74
49
  */
75
- function parsePackagesArg(appsDir) {
50
+ export function parseArgs() {
51
+ const libsArg = process.argv.find((arg) => arg === '--libraries' || arg === '--library');
76
52
  const packagesArg = process.argv.find((arg) => arg === '--packages' || arg === '--package');
77
- if (!packagesArg) return null;
53
+ const appsArg = process.argv.find((arg) => arg === '--apps' || arg === '--app');
54
+
55
+ const moduleModeArgs = libsArg || packagesArg || appsArg;
78
56
 
79
- const index = process.argv.indexOf(packagesArg);
57
+ if (!moduleModeArgs) return null;
58
+
59
+ const index = process.argv.indexOf(moduleModeArgs);
80
60
  const rawValue = process.argv[index + 1] || '';
81
- const packageNames = rawValue
61
+
62
+ const modulePatterns = rawValue
82
63
  .split(',')
83
64
  .map((n) => n.trim())
84
65
  .filter(Boolean);
85
66
 
86
- const mappedFolders = [];
87
- for (const pkgName of packageNames) {
88
- const folder = extractAppFolderFromPackageName(pkgName);
89
- if (folder && fs.existsSync(path.join(appsDir, folder))) {
90
- mappedFolders.push(folder);
67
+ /** @type {ModuleDescriptor[]} */
68
+ const modules = [];
69
+
70
+ for (const modulePattern of modulePatterns) {
71
+ const discoveredModule = getModule(modulePattern, !!libsArg);
72
+ if (discoveredModule) {
73
+ modules.push(discoveredModule);
91
74
  } else {
92
- logError(`❌ Package ${pkgName} → app folder ${folder} not found, skipping`);
75
+ logError(
76
+ `❌ Module ${modulePattern} → folder ${discoveredModule?.shortPath || 'unknown'} not found, skipping`,
77
+ );
93
78
  }
94
79
  }
95
80
 
96
81
  logInfo(
97
- `Running in packages mode for: ${packageNames.join(', ')} → apps: ${mappedFolders.join(', ')}`,
82
+ `patterns ${modulePatterns.join(', ')} → folders: ${modules
83
+ .map((item) => item.fullPath)
84
+ .join(', ')}`,
98
85
  );
99
- return { appFolders: mappedFolders, packageNames };
100
- }
101
86
 
102
- /**
103
- * Discover all React apps in the apps directory when no CLI flags are provided.
104
- *
105
- * Only apps that contain a `package.json` file and are identified as React apps
106
- * (via `isReactApp`) will be included.
107
- *
108
- * @param {string} appsDir - Absolute path to the apps root directory.
109
- * @returns {{appFolders: string[], packageNames: string[]}}
110
- * Object containing discovered app folders and an empty `packageNames` array.
111
- */
112
- function discoverApps(appsDir) {
113
- const discoveredAppFolders = fs.readdirSync(appsDir).filter((dir) => {
114
- const pkgPath = path.join(appsDir, dir, 'package.json');
115
- return fs.existsSync(pkgPath) && isReactApp(pkgPath);
116
- });
117
-
118
- if (discoveredAppFolders.length === 0) {
119
- logWarn('⚠️ No React apps found to analyze.');
120
- return { appFolders: [], packageNames: [] };
121
- }
122
-
123
- logInfo(`Running in auto-discovery mode (${discoveredAppFolders.length} apps).`);
124
- return { appFolders: discoveredAppFolders, packageNames: [] };
87
+ return modules;
125
88
  }
126
89
 
127
90
  /**
128
- * Parse CLI arguments to resolve which apps or packages should be analyzed.
91
+ * Alias for {@link parseArgs}, used for semantic clarity in CLI entrypoints.
129
92
  *
130
- * Priority order:
131
- * 1. Explicit `--apps` flag
132
- * 2. Explicit `--packages` flag
133
- * 3. Auto-discovery fallback
93
+ * This function simply delegates to {@link parseArgs} and returns the same result.
134
94
  *
135
- * @param {string} appsDir - Absolute path to the apps root directory.
136
- * @returns {{appFolders: string[], packageNames: string[]}}
137
- * Object containing app folder names and package names for analysis.
95
+ * @returns {ModuleDescriptor[] | null}
96
+ * Array of resolved module descriptors, or `null` if no valid flags were provided.
97
+ *
98
+ * @example
99
+ * parseCliTargets();
100
+ * // Equivalent to parseArgs(), typically used in CLI commands.
138
101
  */
139
- export function parseCliTargets(appsDir) {
140
- return parseAppsArg(appsDir) || parsePackagesArg(appsDir) || discoverApps(appsDir);
102
+ export function parseCliTargets() {
103
+ return parseArgs();
141
104
  }
142
105
 
143
106
  /**
@@ -217,6 +180,13 @@ export function buildCodeDuplicationArgs(outputDir) {
217
180
  // https://github.com/kucherenko/jscpd/blob/master/apps/jscpd/README.md#ignore
218
181
  args.push('--ignore', cfg.ignore.join(','));
219
182
  }
220
-
183
+ // jscpd accepts regex patterns via `--ignore-pattern` (singular).
184
+ // Multiple regexes should be joined by comma.
185
+ // https://github.com/kucherenko/jscpd/blob/master/apps/jscpd/README.md#ignore-pattern
186
+ if (Array.isArray(cfg.ignorePatterns) && cfg.ignorePatterns.length > 0) {
187
+ for (const pat of cfg.ignorePatterns) {
188
+ args.push('--ignore-pattern', `'${pat}'`);
189
+ }
190
+ }
221
191
  return args;
222
192
  }