dependency-cruiser 10.7.0 → 10.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.
@@ -28,6 +28,7 @@ try {
28
28
  "-T, --output-type <type>",
29
29
  "output type; e.g. err, err-html, dot, ddot, archi, flat, baseline or json\n(default: err)"
30
30
  )
31
+ .option("-m, --metrics", "calculate stability metrics", false)
31
32
  .option(
32
33
  "-f, --output-to <file>",
33
34
  "file to write output to; - for stdout",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dependency-cruiser",
3
- "version": "10.7.0",
3
+ "version": "10.8.0",
4
4
  "description": "Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.",
5
5
  "keywords": [
6
6
  "static analysis",
@@ -131,13 +131,13 @@
131
131
  "version": "npm-run-all build depcruise:graph:doc scm:stage"
132
132
  },
133
133
  "dependencies": {
134
- "acorn": "8.5.0",
134
+ "acorn": "8.6.0",
135
135
  "acorn-jsx": "5.3.2",
136
136
  "acorn-jsx-walk": "2.0.0",
137
137
  "acorn-loose": "8.2.1",
138
138
  "acorn-walk": "8.2.0",
139
- "ajv": "8.6.3",
140
- "chalk": "4.1.2",
139
+ "ajv": "8.8.2",
140
+ "chalk": "^4.1.2",
141
141
  "commander": "8.3.0",
142
142
  "enhanced-resolve": "5.8.3",
143
143
  "figures": "^3.2.0",
@@ -152,17 +152,17 @@
152
152
  "semver": "^7.3.5",
153
153
  "semver-try-require": "^5.0.1",
154
154
  "teamcity-service-messages": "0.1.11",
155
- "tsconfig-paths-webpack-plugin": "3.5.1",
155
+ "tsconfig-paths-webpack-plugin": "3.5.2",
156
156
  "wrap-ansi": "^7.0.0"
157
157
  },
158
158
  "devDependencies": {
159
- "@babel/core": "7.15.8",
160
- "@babel/plugin-transform-modules-commonjs": "7.15.4",
161
- "@babel/preset-typescript": "7.15.0",
162
- "@swc/core": "1.2.105",
163
- "@typescript-eslint/eslint-plugin": "5.2.0",
164
- "@typescript-eslint/parser": "5.2.0",
165
- "@vue/compiler-sfc": "3.2.20",
159
+ "@babel/core": "7.16.0",
160
+ "@babel/plugin-transform-modules-commonjs": "7.16.0",
161
+ "@babel/preset-typescript": "7.16.0",
162
+ "@swc/core": "1.2.113",
163
+ "@typescript-eslint/eslint-plugin": "5.4.0",
164
+ "@typescript-eslint/parser": "5.4.0",
165
+ "@vue/compiler-sfc": "3.2.23",
166
166
  "c8": "7.10.0",
167
167
  "chai": "4.3.4",
168
168
  "chai-json-schema": "1.5.1",
@@ -171,23 +171,23 @@
171
171
  "eslint-config-moving-meadow": "2.0.9",
172
172
  "eslint-config-prettier": "8.3.0",
173
173
  "eslint-plugin-budapestian": "3.0.1",
174
- "eslint-plugin-import": "2.25.2",
174
+ "eslint-plugin-import": "2.25.3",
175
175
  "eslint-plugin-mocha": "9.0.0",
176
176
  "eslint-plugin-node": "11.1.0",
177
177
  "eslint-plugin-security": "1.4.0",
178
- "eslint-plugin-unicorn": "37.0.1",
178
+ "eslint-plugin-unicorn": "39.0.0",
179
179
  "husky": "^4.3.8",
180
180
  "intercept-stdout": "0.1.2",
181
- "lint-staged": "11.2.6",
181
+ "lint-staged": "12.1.2",
182
182
  "mocha": "9.1.3",
183
183
  "normalize-newline": "^3.0.0",
184
184
  "npm-run-all": "4.1.5",
185
- "prettier": "2.4.1",
185
+ "prettier": "2.5.0",
186
186
  "proxyquire": "2.1.3",
187
187
  "shx": "0.3.3",
188
- "svelte": "3.44.0",
188
+ "svelte": "3.44.2",
189
189
  "symlink-dir": "5.0.1",
190
- "typescript": "4.4.4",
190
+ "typescript": "4.5.2",
191
191
  "upem": "^7.0.0",
192
192
  "vue-template-compiler": "2.6.14",
193
193
  "yarn": "1.22.17"
@@ -199,6 +199,11 @@
199
199
  "policy": "wanted",
200
200
  "because": "some eslint plugins (eslint-plugin-budapestian) are not compatible with eslint 8 yet "
201
201
  },
202
+ {
203
+ "package": "chalk",
204
+ "policy": "wanted",
205
+ "because": "version 5 only exports ejs - and we use cjs and don't transpile"
206
+ },
202
207
  {
203
208
  "package": "figures",
204
209
  "policy": "wanted",
@@ -21,6 +21,7 @@ const KNOWN_DEPCRUISE_CLI_OPTIONS = [
21
21
  "info",
22
22
  "init",
23
23
  "maxDepth",
24
+ "metrics",
24
25
  "moduleSystems",
25
26
  "outputTo",
26
27
  "outputType",
@@ -11,7 +11,12 @@ function hasDependentsRule(pOptions) {
11
11
  }
12
12
 
13
13
  function shouldAddDependents(pOptions) {
14
- return Boolean(pOptions.forceDeriveDependents) || hasDependentsRule(pOptions);
14
+ return (
15
+ Boolean(pOptions.forceDeriveDependents) ||
16
+ Boolean(pOptions.metrics) ||
17
+ pOptions.outputType === "metrics" ||
18
+ hasDependentsRule(pOptions)
19
+ );
15
20
  }
16
21
 
17
22
  module.exports = function addDependents(pModules, pOptions) {
@@ -0,0 +1,9 @@
1
+ const getStabilityMetrics = require("./get-stability-metrics");
2
+ const { shouldDeriveMetrics } = require("./utl");
3
+
4
+ module.exports = function deriveMetrics(pModules, pOptions) {
5
+ if (shouldDeriveMetrics(pOptions)) {
6
+ return { folders: getStabilityMetrics(pModules) };
7
+ }
8
+ return {};
9
+ };
@@ -0,0 +1,97 @@
1
+ /* eslint-disable security/detect-object-injection */
2
+ const path = require("path").posix;
3
+ const { foldersObject2folderArray, getParentFolders } = require("./utl");
4
+ const {
5
+ getAfferentCouplings,
6
+ getEfferentCouplings,
7
+ metricsAreCalculable,
8
+ } = require("./module-utl");
9
+
10
+ function upsertCouplings(pAllDependents, pNewDependents) {
11
+ pNewDependents.forEach((pNewDependent) => {
12
+ pAllDependents[pNewDependent] = pAllDependents[pNewDependent] || {
13
+ count: 0,
14
+ };
15
+ pAllDependents[pNewDependent].count += 1;
16
+ });
17
+ }
18
+
19
+ function upsertFolderAttributes(pAllMetrics, pModule, pDirname) {
20
+ pAllMetrics[pDirname] = pAllMetrics[pDirname] || {
21
+ dependencies: {},
22
+ dependents: {},
23
+ moduleCount: 0,
24
+ };
25
+
26
+ upsertCouplings(
27
+ pAllMetrics[pDirname].dependents,
28
+ getAfferentCouplings(pModule, pDirname)
29
+ );
30
+ upsertCouplings(
31
+ pAllMetrics[pDirname].dependencies,
32
+ getEfferentCouplings(pModule, pDirname).map(
33
+ (pDependency) => pDependency.resolved
34
+ )
35
+ );
36
+ pAllMetrics[pDirname].moduleCount += 1;
37
+
38
+ return pAllMetrics;
39
+ }
40
+
41
+ function orderFolderMetrics(pLeftMetric, pRightMetric) {
42
+ // return pLeft.name.localeCompare(pRight.name);
43
+ // For intended use in a table it's probably more useful to sort by
44
+ // instability. Might need to be either configurable or flexible
45
+ // in the output, though
46
+ return pRightMetric.instability - pLeftMetric.instability;
47
+ }
48
+
49
+ function sumCounts(pAll, pCurrent) {
50
+ return pAll + pCurrent.count;
51
+ }
52
+
53
+ function getFolderLevelCouplings(pCouplingArray) {
54
+ return Array.from(
55
+ new Set(
56
+ pCouplingArray.map((pCoupling) =>
57
+ path.dirname(pCoupling.name) === "."
58
+ ? pCoupling.name
59
+ : path.dirname(pCoupling.name)
60
+ )
61
+ )
62
+ );
63
+ }
64
+
65
+ function calculateFolderMetrics(pFolder) {
66
+ const lModuleDependents = foldersObject2folderArray(pFolder.dependents);
67
+ const lModuleDependencies = foldersObject2folderArray(pFolder.dependencies);
68
+ const lAfferentCouplings = lModuleDependents.reduce(sumCounts, 0);
69
+ const lEfferentCouplings = lModuleDependencies.reduce(sumCounts, 0);
70
+
71
+ return {
72
+ ...pFolder,
73
+ afferentCouplings: lAfferentCouplings,
74
+ efferentCouplings: lEfferentCouplings,
75
+ // when both afferentCouplings and efferentCouplings equal 0 instability will
76
+ // yield NaN. Judging Bob Martin's intention, a component with no outgoing
77
+ // dependencies is maximum stable (0)
78
+ instability:
79
+ lEfferentCouplings / (lEfferentCouplings + lAfferentCouplings) || 0,
80
+ dependents: getFolderLevelCouplings(lModuleDependents),
81
+ dependencies: getFolderLevelCouplings(lModuleDependencies),
82
+ };
83
+ }
84
+
85
+ module.exports = function getStabilityMetrics(pModules) {
86
+ return foldersObject2folderArray(
87
+ pModules.filter(metricsAreCalculable).reduce((pAllFolders, pModule) => {
88
+ getParentFolders(path.dirname(pModule.source)).forEach(
89
+ (pParentDirectory) =>
90
+ upsertFolderAttributes(pAllFolders, pModule, pParentDirectory)
91
+ );
92
+ return pAllFolders;
93
+ }, {})
94
+ )
95
+ .map(calculateFolderMetrics)
96
+ .sort(orderFolderMetrics);
97
+ };
@@ -0,0 +1,27 @@
1
+ const path = require("path").posix;
2
+
3
+ function getAfferentCouplings(pModule, pDirname) {
4
+ return pModule.dependents.filter(
5
+ (pDependent) => !pDependent.startsWith(pDirname.concat(path.sep))
6
+ );
7
+ }
8
+
9
+ function getEfferentCouplings(pModule, pDirname) {
10
+ return pModule.dependencies.filter(
11
+ (pDependency) => !pDependency.resolved.startsWith(pDirname.concat(path.sep))
12
+ );
13
+ }
14
+
15
+ function metricsAreCalculable(pModule) {
16
+ return (
17
+ !pModule.coreModule &&
18
+ !pModule.couldNotResolve &&
19
+ !pModule.matchesDoNotFollow
20
+ );
21
+ }
22
+
23
+ module.exports = {
24
+ getAfferentCouplings,
25
+ getEfferentCouplings,
26
+ metricsAreCalculable,
27
+ };
@@ -0,0 +1,29 @@
1
+ // const { findModuleByName, clearCache } = require("../utl");
2
+ const { metricsAreCalculable } = require("./module-utl");
3
+ const { shouldDeriveMetrics } = require("./utl");
4
+
5
+ module.exports = function deriveModuleMetrics(pModules, pOptions) {
6
+ if (shouldDeriveMetrics(pOptions)) {
7
+ return pModules.map((pModule) => ({
8
+ ...pModule,
9
+ ...(metricsAreCalculable(pModule)
10
+ ? {
11
+ instability:
12
+ pModule.dependencies.length /
13
+ (pModule.dependents.length + pModule.dependencies.length) || 0,
14
+ }
15
+ : {}),
16
+ }));
17
+ // clearCache();
18
+ // return lModules.map((pModule) => ({
19
+ // ...pModule,
20
+ // dependencies: pModule.dependencies.map((pDependency) => ({
21
+ // ...pDependency,
22
+ // instability:
23
+ // (findModuleByName(lModules, pDependency.resolved) || {})
24
+ // .instability || 0,
25
+ // })),
26
+ // }));
27
+ }
28
+ return pModules;
29
+ };
@@ -0,0 +1,28 @@
1
+ /* eslint-disable security/detect-object-injection */
2
+ function getParentFolders(pPath) {
3
+ let lFragments = pPath.split("/");
4
+ let lReturnValue = [];
5
+
6
+ while (lFragments.length > 0) {
7
+ lReturnValue.push(lFragments.join("/"));
8
+ lFragments.pop();
9
+ }
10
+ return lReturnValue.reverse();
11
+ }
12
+
13
+ function foldersObject2folderArray(pObject) {
14
+ return Object.keys(pObject).map((pKey) => ({
15
+ name: pKey,
16
+ ...pObject[pKey],
17
+ }));
18
+ }
19
+
20
+ function shouldDeriveMetrics(pOptions) {
21
+ return pOptions.metrics || pOptions.outputType === "metrics";
22
+ }
23
+
24
+ module.exports = {
25
+ getParentFolders,
26
+ foldersObject2folderArray,
27
+ shouldDeriveMetrics,
28
+ };
@@ -1,14 +1,26 @@
1
- const _memoize = require("lodash/memoize");
1
+ let gIndexedGraph = null;
2
2
 
3
- function bareFindModuleByName(pGraph, pSource) {
4
- return pGraph.find((pNode) => pNode.source === pSource);
3
+ /**
4
+ * Returns the module with attribute pSource, when it exists in the pModuleGraph.
5
+ * Returns undefined when it doesn't.
6
+ *
7
+ * This function uses an indexed cache for efficiency reasons. If you need to
8
+ * call this function consecutively for different module graphs, you can clear
9
+ * this cache with the clearCache function from this module.
10
+ *
11
+ * @param {import('../../../types/cruise-result').IModule[]} pModuleGraph
12
+ * @param {string} pSource
13
+ * @returns {import('../../../types/cruise-result').IModule | undefined}
14
+ */
15
+ function findModuleByName(pModuleGraph, pSource) {
16
+ if (!gIndexedGraph) {
17
+ gIndexedGraph = new Map(
18
+ pModuleGraph.map((pModule) => [pModule.source, pModule])
19
+ );
20
+ }
21
+ return gIndexedGraph.get(pSource);
5
22
  }
6
23
 
7
- const findModuleByName = _memoize(
8
- bareFindModuleByName,
9
- (_pGraph, pSource) => pSource
10
- );
11
-
12
24
  function isDependent(pResolvedName) {
13
25
  return (pModule) =>
14
26
  pModule.dependencies.some(
@@ -17,7 +29,11 @@ function isDependent(pResolvedName) {
17
29
  }
18
30
 
19
31
  function clearCache() {
20
- findModuleByName.cache.clear();
32
+ gIndexedGraph = null;
21
33
  }
22
34
 
23
- module.exports = { findModuleByName, clearCache, isDependent };
35
+ module.exports = {
36
+ findModuleByName,
37
+ clearCache,
38
+ isDependent,
39
+ };
@@ -8,6 +8,7 @@ const addDependents = require("./derive/dependents");
8
8
  const deriveReachable = require("./derive/reachable");
9
9
  const addValidations = require("./add-validations");
10
10
  const softenKnownViolations = require("./soften-known-violations");
11
+ const deriveModuleMetrics = require("./derive/metrics/module");
11
12
 
12
13
  module.exports = function enrichModules(pModules, pOptions) {
13
14
  bus.emit("progress", "analyzing: cycles", { level: busLogLevels.INFO });
@@ -18,6 +19,10 @@ module.exports = function enrichModules(pModules, pOptions) {
18
19
  lModules = deriveOrphans(lModules);
19
20
  bus.emit("progress", "analyzing: reachables", { level: busLogLevels.INFO });
20
21
  lModules = deriveReachable(lModules, pOptions.ruleSet);
22
+ bus.emit("progress", "analyzing: calculating module metrics", {
23
+ level: busLogLevels.INFO,
24
+ });
25
+ lModules = deriveModuleMetrics(lModules, pOptions);
21
26
  bus.emit("progress", "analyzing: add focus (if any)", {
22
27
  level: busLogLevels.INFO,
23
28
  });
@@ -1,4 +1,5 @@
1
1
  const enrichModules = require("./enrich-modules");
2
+ const deriveFolderMetrics = require("./derive/metrics/folder.js");
2
3
  const summarize = require("./summarize");
3
4
  const clearCaches = require("./clear-caches");
4
5
 
@@ -9,6 +10,7 @@ module.exports = function enrich(pModules, pOptions, pFileAndDirectoryArray) {
9
10
  clearCaches();
10
11
  return {
11
12
  modules: lModules,
13
+ ...deriveFolderMetrics(lModules, pOptions),
12
14
  summary: summarize(lModules, pOptions, pFileAndDirectoryArray),
13
15
  };
14
16
  };
@@ -78,7 +78,8 @@ function softenKnownViolation(pModule, pKnownViolations, pSoftenedSeverity) {
78
78
  pRule,
79
79
  pModule.source,
80
80
  pKnownViolations.filter(
81
- (pKnownError) => pKnownError.from === pKnownError.to
81
+ (pKnownError) =>
82
+ pKnownError.from === pKnownError.to && !pKnownError.cycle
82
83
  ),
83
84
  pSoftenedSeverity
84
85
  )
@@ -93,7 +94,8 @@ function softenKnownViolation(pModule, pKnownViolations, pSoftenedSeverity) {
93
94
  pDependency,
94
95
  pModule.source,
95
96
  pKnownViolations.filter(
96
- (pKnownError) => pKnownError.from !== pKnownError.to
97
+ (pKnownError) =>
98
+ pKnownError.from !== pKnownError.to || pKnownError.cycle
97
99
  ),
98
100
  pSoftenedSeverity
99
101
  )
@@ -119,7 +119,13 @@ if (VisitorModule) {
119
119
  // also include the same method, but with the correct spelling.
120
120
  visitExportAllDeclration(pNode) {
121
121
  this.pushImportExportSource(pNode);
122
- return super.visitExportAllDeclration(pNode);
122
+ /* c8 ignore start */
123
+ if (super.visitExportAllDeclration) {
124
+ return super.visitExportAllDeclration(pNode);
125
+ } else {
126
+ /* c8 ignore stop */
127
+ return super.visitExportAllDeclaration(pNode);
128
+ }
123
129
  }
124
130
 
125
131
  /* c8 ignore start */
@@ -131,7 +137,13 @@ if (VisitorModule) {
131
137
  // same spelling error as the above - same solution
132
138
  visitExportNamedDeclration(pNode) {
133
139
  this.pushImportExportSource(pNode);
134
- return super.visitExportNamedDeclration(pNode);
140
+ /* c8 ignore start */
141
+ if (super.visitExportNamedDeclration) {
142
+ return super.visitExportNamedDeclration(pNode);
143
+ } else {
144
+ /* c8 ignore stop */
145
+ return super.visitExportNamedDeclaration(pNode);
146
+ }
135
147
  }
136
148
  /* c8 ignore start */
137
149
  visitExportNamedDeclaration(pNode) {
@@ -28,13 +28,18 @@ function reSummarizeResults(pResult, pFormatOptions) {
28
28
 
29
29
  module.exports = function reportWrap(pResult, pFormatOptions) {
30
30
  const lReportFunction = report.getReporter(pFormatOptions.outputType);
31
+ const lReportOptions = _get(
32
+ pResult,
33
+ `summary.optionsUsed.reporterOptions.${pFormatOptions.outputType}`,
34
+ {}
35
+ );
31
36
 
32
37
  return lReportFunction(
33
38
  reSummarizeResults(pResult, pFormatOptions),
34
39
  // passing format options here so reporters that read collapse patterns
35
40
  // from the result take the one passed in the format options instead
36
41
  _has(pFormatOptions, "collapse")
37
- ? { collapsePattern: pFormatOptions.collapse }
38
- : {}
42
+ ? { ...lReportOptions, collapsePattern: pFormatOptions.collapse }
43
+ : lReportOptions
39
44
  );
40
45
  };
package/src/meta.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /* generated - don't edit */
2
2
 
3
3
  module.exports = {
4
- version: "10.7.0",
4
+ version: "10.8.0",
5
5
  engines: {
6
6
  node: "^12.20||^14||>=16",
7
7
  },
@@ -13,6 +13,7 @@ const json = require("./json");
13
13
  const teamcity = require("./teamcity");
14
14
  const text = require("./text");
15
15
  const baseline = require("./baseline");
16
+ const metrics = require("./metrics");
16
17
  const { getExternalPluginReporter } = require("./plugins");
17
18
 
18
19
  const TYPE2REPORTER = {
@@ -32,6 +33,7 @@ const TYPE2REPORTER = {
32
33
  teamcity,
33
34
  text,
34
35
  baseline,
36
+ metrics,
35
37
  };
36
38
 
37
39
  /**
@@ -0,0 +1,131 @@
1
+ const { EOL } = require("os");
2
+ const chalk = require("chalk");
3
+
4
+ const DECIMAL_BASE = 10;
5
+ const METRIC_WIDTH = 4;
6
+ const INSTABILITY_DECIMALS = 2;
7
+ const YADDUM = DECIMAL_BASE ** INSTABILITY_DECIMALS;
8
+ const COMPONENT_HEADER = "name";
9
+
10
+ function getHeader(pMaxNameWidth) {
11
+ return `${COMPONENT_HEADER.padEnd(pMaxNameWidth)} ${"N".padStart(
12
+ METRIC_WIDTH + 1
13
+ )} ${"Ca".padStart(METRIC_WIDTH + 1)} ${"Ce".padStart(
14
+ METRIC_WIDTH + 1
15
+ )} ${"I".padEnd(METRIC_WIDTH + 1)}`;
16
+ }
17
+
18
+ function getDemarcationLine(pMaxNameWidth) {
19
+ return `${"-".repeat(pMaxNameWidth)} ${"-".repeat(
20
+ METRIC_WIDTH + 1
21
+ )} ${"-".repeat(METRIC_WIDTH + 1)} ${"-".repeat(
22
+ METRIC_WIDTH + 1
23
+ )} ${"-".repeat(METRIC_WIDTH + 1)}`;
24
+ }
25
+
26
+ function getMetricsTable(pMetrics, pMaxNameWidth) {
27
+ return pMetrics.map(
28
+ ({
29
+ name,
30
+ moduleCount,
31
+ afferentCouplings,
32
+ efferentCouplings,
33
+ instability,
34
+ }) =>
35
+ `${name.padEnd(pMaxNameWidth, " ")} ${moduleCount
36
+ .toString(DECIMAL_BASE)
37
+ .padStart(METRIC_WIDTH)} ${afferentCouplings
38
+ .toString(DECIMAL_BASE)
39
+ .padStart(METRIC_WIDTH)} ${efferentCouplings
40
+ .toString(DECIMAL_BASE)
41
+ .padStart(METRIC_WIDTH)} ${(Math.round(YADDUM * instability) / YADDUM)
42
+ .toString(DECIMAL_BASE)
43
+ .padEnd(METRIC_WIDTH)}`
44
+ );
45
+ }
46
+
47
+ function metricifyModule({ source, dependents, dependencies, instability }) {
48
+ return {
49
+ name: source,
50
+ moduleCount: 1,
51
+ afferentCouplings: dependents.length,
52
+ efferentCouplings: dependencies.length,
53
+ instability,
54
+ };
55
+ }
56
+
57
+ function metricsAreCalculable(pModule) {
58
+ return Object.prototype.hasOwnProperty.call(pModule, "instability");
59
+ }
60
+
61
+ function orderByNumber(pAttributeName) {
62
+ // eslint-disable-next-line security/detect-object-injection
63
+ return (pLeft, pRight) => pRight[pAttributeName] - pLeft[pAttributeName];
64
+ }
65
+
66
+ function orderByString(pAttributeName) {
67
+ return (pLeft, pRight) =>
68
+ // eslint-disable-next-line security/detect-object-injection
69
+ pLeft[pAttributeName].localeCompare(pRight[pAttributeName]);
70
+ }
71
+
72
+ function transformMetricsToTable(
73
+ { modules, folders },
74
+ { orderBy, hideFolders, hideModules }
75
+ ) {
76
+ // TODO: should probably use a table module for this (i.e. text-table)
77
+ // to simplify this code
78
+ let lComponents = [];
79
+ lComponents = lComponents.concat(hideFolders ? [] : folders);
80
+ lComponents = lComponents.concat(
81
+ hideModules ? [] : modules.filter(metricsAreCalculable).map(metricifyModule)
82
+ );
83
+ const lMaxNameWidth = lComponents
84
+ .map(({ name }) => name.length)
85
+ .concat(COMPONENT_HEADER.length)
86
+ .sort((pLeft, pRight) => pLeft - pRight)
87
+ .pop();
88
+
89
+ return [chalk.bold(getHeader(lMaxNameWidth))]
90
+ .concat(getDemarcationLine(lMaxNameWidth))
91
+ .concat(
92
+ getMetricsTable(
93
+ lComponents
94
+ .sort(orderByString("name"))
95
+ .sort(orderByNumber(orderBy || "instability")),
96
+ lMaxNameWidth
97
+ )
98
+ )
99
+ .join(EOL)
100
+ .concat(EOL);
101
+ }
102
+
103
+ /**
104
+ * returns stability metrics of modules & folders in an ascii table
105
+ *
106
+ * Potential future features:
107
+ * - additional output formats (csv?, html?)
108
+ *
109
+ * @param {import('../../types/dependency-cruiser').ICruiseResult} pCruiseResult -
110
+ * the output of a dependency-cruise adhering to dependency-cruiser's
111
+ * cruise result schema
112
+ * @return {import('../../types/dependency-cruiser').IReporterOutput} -
113
+ * output: some metrics on folders and dependencies
114
+ * exitCode: 0
115
+ */
116
+ module.exports = (pCruiseResult, pReporterOptions) => {
117
+ const lReporterOptions = pReporterOptions || {};
118
+ if (pCruiseResult.folders) {
119
+ return {
120
+ output: transformMetricsToTable(pCruiseResult, lReporterOptions),
121
+ exitCode: 0,
122
+ };
123
+ } else {
124
+ return {
125
+ output:
126
+ `${EOL}ERROR: The cruise result didn't contain any metrics - re-running the cruise with${EOL}` +
127
+ ` the '--metrics' command line option should fix that.${EOL}${EOL}`,
128
+ exitCode: 1,
129
+ };
130
+ }
131
+ };
@@ -394,6 +394,7 @@ module.exports = {
394
394
  dot: { $ref: "#/definitions/DotReporterOptionsType" },
395
395
  ddot: { $ref: "#/definitions/DotReporterOptionsType" },
396
396
  flat: { $ref: "#/definitions/DotReporterOptionsType" },
397
+ metrics: { $ref: "#/definitions/MetricsReporterOptionsType" },
397
398
  },
398
399
  },
399
400
  AnonReporterOptionsType: {
@@ -401,6 +402,24 @@ module.exports = {
401
402
  additionalProperties: false,
402
403
  properties: { wordlist: { type: "array", items: { type: "string" } } },
403
404
  },
405
+ MetricsReporterOptionsType: {
406
+ type: "object",
407
+ additionalProperties: false,
408
+ properties: {
409
+ orderBy: {
410
+ type: "string",
411
+ enum: [
412
+ "instability",
413
+ "moduleCount",
414
+ "afferentCouplings",
415
+ "efferentCouplings",
416
+ "name",
417
+ ],
418
+ },
419
+ hideModules: { type: "boolean" },
420
+ hideFolders: { type: "boolean" },
421
+ },
422
+ },
404
423
  DotReporterOptionsType: {
405
424
  type: "object",
406
425
  additionalProperties: false,
@@ -9,6 +9,7 @@ module.exports = {
9
9
  additionalProperties: false,
10
10
  properties: {
11
11
  modules: { $ref: "#/definitions/ModulesType" },
12
+ folders: { $ref: "#/definitions/FoldersType" },
12
13
  summary: { $ref: "#/definitions/SummaryType" },
13
14
  },
14
15
  definitions: {
@@ -46,6 +47,7 @@ module.exports = {
46
47
  items: { $ref: "#/definitions/RuleSummaryType" },
47
48
  },
48
49
  consolidated: { type: "boolean" },
50
+ instability: { type: "number" },
49
51
  },
50
52
  },
51
53
  ReachableType: {
@@ -158,6 +160,21 @@ module.exports = {
158
160
  },
159
161
  },
160
162
  SeverityType: { type: "string", enum: ["error", "warn", "info", "ignore"] },
163
+ FoldersType: { type: "array", items: { $ref: "#/definitions/FolderType" } },
164
+ FolderType: {
165
+ type: "object",
166
+ required: ["name", "moduleCount"],
167
+ additionalProperties: false,
168
+ properties: {
169
+ name: { type: "string" },
170
+ dependents: { type: "array", items: { type: "string" } },
171
+ dependencies: { type: "array", items: { type: "string" } },
172
+ moduleCount: { type: "number" },
173
+ afferentCouplings: { type: "number" },
174
+ efferentCouplings: { type: "number" },
175
+ instability: { type: "number" },
176
+ },
177
+ },
161
178
  SummaryType: {
162
179
  type: "object",
163
180
  required: [
@@ -510,6 +527,7 @@ module.exports = {
510
527
  "teamcity",
511
528
  "anon",
512
529
  "text",
530
+ "metrics",
513
531
  ],
514
532
  },
515
533
  { type: "string", pattern: "^plugin:[^:]+$" },
@@ -553,6 +571,7 @@ module.exports = {
553
571
  dot: { $ref: "#/definitions/DotReporterOptionsType" },
554
572
  ddot: { $ref: "#/definitions/DotReporterOptionsType" },
555
573
  flat: { $ref: "#/definitions/DotReporterOptionsType" },
574
+ metrics: { $ref: "#/definitions/MetricsReporterOptionsType" },
556
575
  },
557
576
  },
558
577
  AnonReporterOptionsType: {
@@ -560,6 +579,24 @@ module.exports = {
560
579
  additionalProperties: false,
561
580
  properties: { wordlist: { type: "array", items: { type: "string" } } },
562
581
  },
582
+ MetricsReporterOptionsType: {
583
+ type: "object",
584
+ additionalProperties: false,
585
+ properties: {
586
+ orderBy: {
587
+ type: "string",
588
+ enum: [
589
+ "instability",
590
+ "moduleCount",
591
+ "afferentCouplings",
592
+ "efferentCouplings",
593
+ "name",
594
+ ],
595
+ },
596
+ hideModules: { type: "boolean" },
597
+ hideFolders: { type: "boolean" },
598
+ },
599
+ },
563
600
  DotReporterOptionsType: {
564
601
  type: "object",
565
602
  additionalProperties: false,
@@ -12,6 +12,15 @@ export interface ICruiseResult {
12
12
  * A list of modules, with for each module the modules it depends upon
13
13
  */
14
14
  modules: IModule[];
15
+ /**
16
+ * A list of folders, as derived from the detected modules, with for each
17
+ * "folder a bunch of metrics (adapted from 'Agile software development:
18
+ * "principles, patterns, and practices' by Robert C Martin (ISBN 0-13-597444-5).
19
+ * "Note: these metrics substitute 'components' and 'classes' from that book
20
+ * "with 'folders' and 'modules'; the closest relatives that work for the most
21
+ * "programming styles in JavaScript (and its derivative languages).
22
+ */
23
+ folders?: IFolder[];
15
24
  /**
16
25
  * Data summarizing the found dependencies
17
26
  */
@@ -97,6 +106,15 @@ export interface IModule {
97
106
  * purposes - it will not be present after a regular cruise.
98
107
  */
99
108
  consolidated?: boolean;
109
+ /**
110
+ * "number of dependents/ (number of dependents + number of dependencies)
111
+ * A measure for how stable the module is; ranging between 0 (completely
112
+ * stable module) to 1 (completely instable module). Derived from Uncle
113
+ * Bob's instability metric - but applied to a single module instead of
114
+ * to a group of them. This attribute is only present when dependency-cruiser
115
+ * was asked to calculate metrics.
116
+ */
117
+ instability?: number;
100
118
  }
101
119
 
102
120
  export interface IDependency {
@@ -393,3 +411,47 @@ export interface IViolation {
393
411
  */
394
412
  via?: string[];
395
413
  }
414
+
415
+ export interface IFolder {
416
+ /**
417
+ * The name of the folder. FOlder names are normalized to posix (so
418
+ * separated by forward slashes e.g.: src/things/morethings)
419
+ */
420
+ name: string;
421
+ /**
422
+ * List of folders depending on this folder
423
+ */
424
+ dependents?: string[];
425
+ /**
426
+ * List of folders this folder depends upon
427
+ */
428
+ dependencies?: string[];
429
+ /**
430
+ * The total number of modules detected in this folder and its sub-folders
431
+ */
432
+ moduleCount: number;
433
+ /**
434
+ * The number of modules outside this folder that depend on modules
435
+ * within this folder. Only present when dependency-cruiser was
436
+ * "asked to calculate it.
437
+ */
438
+ afferentCouplings?: number;
439
+ /**
440
+ * The number of modules inside this folder that depend on modules
441
+ * outside this folder. Only present when dependency-cruiser was
442
+ * asked to calculate it.
443
+ */
444
+ efferentCouplings?: number;
445
+ /**
446
+ * efferentCouplings/ (afferentCouplings + efferentCouplings)
447
+ *
448
+ * A measure for how stable the folder is; ranging between 0
449
+ * (completely stable folder) to 1 (completely instable folder)
450
+ * Note that while 'instability' has a negative connotation it's also
451
+ * (unavoidable in any meaningful system. It's the basis of Martin's
452
+ * variable component stability principle: 'the instability of a folder
453
+ * should be larger than the folders it depends on'. Only present when
454
+ * dependency-cruiser was asked to calculate it.,
455
+ */
456
+ instability?: number;
457
+ }
@@ -21,6 +21,10 @@ export interface IReporterOptions {
21
21
  * Options to tweak the output of the flat /fdot reporter
22
22
  */
23
23
  flat?: IDotReporterOptions;
24
+ /**
25
+ * Options to tweak the output of the metrics reporter
26
+ */
27
+ metrics?: IMetricsReporterOptions;
24
28
  }
25
29
 
26
30
  export interface IReporterFiltersType {
@@ -96,3 +100,16 @@ export interface IDotThemeEntry {
96
100
  criteria: any;
97
101
  attributes: any;
98
102
  }
103
+
104
+ export interface IMetricsReporterOptions {
105
+ hideModules?: boolean;
106
+ hideFolders?: boolean;
107
+ oderBy?: MetricsOrderByType;
108
+ }
109
+
110
+ export type MetricsOrderByType =
111
+ | "instability"
112
+ | "moduleCount"
113
+ | "afferentCouplings"
114
+ | "efferentCouplings"
115
+ | "name";
@@ -18,6 +18,7 @@ export type OutputType =
18
18
  | "teamcity"
19
19
  | "anon"
20
20
  | "text"
21
+ | "metrics"
21
22
  | string;
22
23
 
23
24
  export type SeverityType = "error" | "warn" | "info" | "ignore";