dependency-cruiser 10.5.0 → 10.8.0-beta-2

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.5.0",
3
+ "version": "10.8.0-beta-2",
4
4
  "description": "Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.",
5
5
  "keywords": [
6
6
  "static analysis",
@@ -138,7 +138,7 @@
138
138
  "acorn-walk": "8.2.0",
139
139
  "ajv": "8.6.3",
140
140
  "chalk": "4.1.2",
141
- "commander": "8.2.0",
141
+ "commander": "8.3.0",
142
142
  "enhanced-resolve": "5.8.3",
143
143
  "figures": "^3.2.0",
144
144
  "get-stream": "^6.0.1",
@@ -159,9 +159,10 @@
159
159
  "@babel/core": "7.15.8",
160
160
  "@babel/plugin-transform-modules-commonjs": "7.15.4",
161
161
  "@babel/preset-typescript": "7.15.0",
162
- "@swc/core": "1.2.98",
163
- "@typescript-eslint/eslint-plugin": "5.0.0",
164
- "@typescript-eslint/parser": "5.0.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",
165
166
  "c8": "7.10.0",
166
167
  "chai": "4.3.4",
167
168
  "chai-json-schema": "1.5.1",
@@ -169,7 +170,7 @@
169
170
  "eslint": "^7.32.0",
170
171
  "eslint-config-moving-meadow": "2.0.9",
171
172
  "eslint-config-prettier": "8.3.0",
172
- "eslint-plugin-budapestian": "2.3.0",
173
+ "eslint-plugin-budapestian": "3.0.1",
173
174
  "eslint-plugin-import": "2.25.2",
174
175
  "eslint-plugin-mocha": "9.0.0",
175
176
  "eslint-plugin-node": "11.1.0",
@@ -177,13 +178,14 @@
177
178
  "eslint-plugin-unicorn": "37.0.1",
178
179
  "husky": "^4.3.8",
179
180
  "intercept-stdout": "0.1.2",
180
- "lint-staged": "11.2.3",
181
+ "lint-staged": "11.2.6",
181
182
  "mocha": "9.1.3",
182
- "normalize-newline": "4.1.0",
183
+ "normalize-newline": "^3.0.0",
183
184
  "npm-run-all": "4.1.5",
184
185
  "prettier": "2.4.1",
186
+ "proxyquire": "2.1.3",
185
187
  "shx": "0.3.3",
186
- "svelte": "3.43.2",
188
+ "svelte": "3.44.0",
187
189
  "symlink-dir": "5.0.1",
188
190
  "typescript": "4.4.4",
189
191
  "upem": "^7.0.0",
@@ -212,6 +214,11 @@
212
214
  "policy": "wanted",
213
215
  "because": "version 5 only exports ejs - and we use cjs and don't transpile"
214
216
  },
217
+ {
218
+ "package": "normalize-newline",
219
+ "policy": "wanted",
220
+ "because": "version 4 only exports ejs - and we use cjs and don't transpile (this only used in unit tests - but one of'em is a cjs one ...)"
221
+ },
215
222
  {
216
223
  "package": "wrap-ansi",
217
224
  "policy": "wanted",
@@ -246,7 +253,8 @@
246
253
  "svelte": ">=3.0.0 <4.0.0",
247
254
  "swc": ">=1.0.0 <2.0.0",
248
255
  "typescript": ">=2.0.0 <5.0.0",
249
- "vue-template-compiler": ">=2.0.0 <3.0.0"
256
+ "vue-template-compiler": ">=2.0.0 <3.0.0",
257
+ "@vue/compiler-sfc": ">=3.0.0 <4.0.0"
250
258
  },
251
259
  "husky": {
252
260
  "hooks": {
@@ -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",
@@ -5,7 +5,7 @@ module.exports = function validateNodeEnvironment(pNodeVersion) {
5
5
  // not using default parameter here because the check should run
6
6
  // run on node 4 as well
7
7
  const lNodeVersion = pNodeVersion || process.versions.node;
8
- const VERSION_ERR = `\nERROR: Your node version (${lNodeVersion}) is not supported. dependency-cruiser
8
+ const lVersionError = `\nERROR: Your node version (${lNodeVersion}) is not supported. dependency-cruiser
9
9
  follows the node.js release cycle and runs on these node versions:
10
10
  ${engines.node}
11
11
  See https://nodejs.org/en/about/releases/ for details.
@@ -13,6 +13,6 @@ module.exports = function validateNodeEnvironment(pNodeVersion) {
13
13
  `;
14
14
 
15
15
  if (!satisfies(lNodeVersion, engines.node)) {
16
- throw new Error(VERSION_ERR);
16
+ throw new Error(lVersionError);
17
17
  }
18
18
  };
@@ -20,11 +20,12 @@ function addDependencyViolations(pModule, pDependency, pRuleSet, pValidate) {
20
20
  * of them added whether or not it is
21
21
  * valid and if not which rules were violated
22
22
  */
23
- module.exports = (pModules, pRuleSet, pValidate) =>
24
- pModules.map((pModule) => ({
23
+ module.exports = (pModules, pRuleSet, pValidate) => {
24
+ return pModules.map((pModule) => ({
25
25
  ...pModule,
26
26
  ...(pValidate ? validate.module(pRuleSet, pModule) : { valid: true }),
27
27
  dependencies: pModule.dependencies.map((pDependency) =>
28
28
  addDependencyViolations(pModule, pDependency, pRuleSet, pValidate)
29
29
  ),
30
30
  }));
31
+ };
@@ -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,13 @@
1
+ const { shouldDeriveMetrics } = require("./utl");
2
+
3
+ module.exports = function deriveModuleMetrics(pModules, pOptions) {
4
+ if (shouldDeriveMetrics(pOptions)) {
5
+ return pModules.map((pModule) => ({
6
+ ...pModule,
7
+ instability:
8
+ pModule.dependencies.length /
9
+ (pModule.dependents.length + pModule.dependencies.length) || 0,
10
+ }));
11
+ }
12
+ return pModules;
13
+ };
@@ -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
+ };
@@ -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
  };
@@ -39,6 +39,7 @@ const TRANSPILER2WRAPPER = {
39
39
  swc,
40
40
  typescript: typeScriptWrap,
41
41
  "vue-template-compiler": vueWrap,
42
+ "@vue/compiler-sfc": vueWrap,
42
43
  };
43
44
 
44
45
  const BABELEABLE_EXTENSIONS = [
@@ -1,15 +1,58 @@
1
+ const isEmpty = require("lodash/isEmpty");
1
2
  const _get = require("lodash/get");
2
3
  const tryRequire = require("semver-try-require");
3
4
  const { supportedTranspilers } = require("../../../src/meta.js");
4
5
 
5
- const vueTemplateCompiler = tryRequire(
6
- "vue-template-compiler",
7
- supportedTranspilers["vue-template-compiler"]
8
- );
6
+ /*
7
+ * vue-template-compiler was replaced by @vue/compiler-sfc for Vue3.
8
+ *
9
+ * if your project uses Vue3, then trying to require vue-template-compiler will
10
+ * cause an incompatibility error - so try @vue/compiler-sfc (which is Vue3's
11
+ * version of vue-template-compiler) if the first one fails
12
+ */
13
+ function getVueTemplateCompiler() {
14
+ let lIsVue3 = false;
15
+
16
+ let lCompiler = tryRequire(
17
+ "vue-template-compiler",
18
+ supportedTranspilers["vue-template-compiler"]
19
+ );
20
+
21
+ if (lCompiler === false) {
22
+ lCompiler = tryRequire(
23
+ "@vue/compiler-sfc",
24
+ supportedTranspilers["@vue/compiler-sfc"]
25
+ );
26
+ lIsVue3 = true;
27
+ }
28
+
29
+ return { lCompiler, lIsVue3 };
30
+ }
31
+
32
+ const { lCompiler: vueTemplateCompiler, lIsVue3: isVue3 } =
33
+ getVueTemplateCompiler();
34
+
35
+ function vue3Transpile(pSource) {
36
+ const parsedComponent = vueTemplateCompiler.parse(pSource);
37
+ const errors = _get(parsedComponent, "errors");
38
+
39
+ if (!isEmpty(errors)) {
40
+ return "";
41
+ }
42
+
43
+ return _get(parsedComponent, "descriptor.script.content", "");
44
+ }
45
+
46
+ function vue2Transpile(pSource) {
47
+ return _get(
48
+ vueTemplateCompiler.parseComponent(pSource),
49
+ "script.content",
50
+ ""
51
+ );
52
+ }
9
53
 
10
54
  module.exports = {
11
55
  isAvailable: () => vueTemplateCompiler !== false,
12
-
13
56
  transpile: (pSource) =>
14
- _get(vueTemplateCompiler.parseComponent(pSource), "script.content", ""),
57
+ isVue3 ? vue3Transpile(pSource) : vue2Transpile(pSource),
15
58
  };
@@ -54,12 +54,12 @@ function normalizeFilterOptions(pOptions, pFilterOptionKeys) {
54
54
  function normalizeCollapse(pCollapse) {
55
55
  let lReturnValue = pCollapse;
56
56
  const lOneOrMoreNonSlashes = "[^/]+";
57
- const FOLDER_PATTERN = `${lOneOrMoreNonSlashes}/`;
58
- const FOLDER_BELOW_NODE_MODULES = `node_modules/${lOneOrMoreNonSlashes}`;
57
+ const lFolderPattern = `${lOneOrMoreNonSlashes}/`;
58
+ const lFolderBelowNodeModules = `node_modules/${lOneOrMoreNonSlashes}`;
59
59
  const lSingleDigitRe = /^\d$/;
60
60
 
61
61
  if (typeof pCollapse === "number" || pCollapse.match(lSingleDigitRe)) {
62
- lReturnValue = `${FOLDER_BELOW_NODE_MODULES}|^${FOLDER_PATTERN.repeat(
62
+ lReturnValue = `${lFolderBelowNodeModules}|^${lFolderPattern.repeat(
63
63
  Number.parseInt(pCollapse, 10)
64
64
  )}`;
65
65
  }
package/src/meta.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /* generated - don't edit */
2
2
 
3
3
  module.exports = {
4
- version: "10.5.0",
4
+ version: "10.8.0-beta-2",
5
5
  engines: {
6
6
  node: "^12.20||^14||>=16",
7
7
  },
@@ -14,5 +14,6 @@ module.exports = {
14
14
  swc: ">=1.0.0 <2.0.0",
15
15
  typescript: ">=2.0.0 <5.0.0",
16
16
  "vue-template-compiler": ">=2.0.0 <3.0.0",
17
+ "@vue/compiler-sfc": ">=3.0.0 <4.0.0",
17
18
  },
18
19
  };
@@ -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,113 @@
1
+ const os = 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((pMetric) => {
28
+ return `${pMetric.name.padEnd(pMaxNameWidth, " ")} ${pMetric.moduleCount
29
+ .toString(DECIMAL_BASE)
30
+ .padStart(METRIC_WIDTH)} ${pMetric.afferentCouplings
31
+ .toString(DECIMAL_BASE)
32
+ .padStart(METRIC_WIDTH)} ${pMetric.efferentCouplings
33
+ .toString(DECIMAL_BASE)
34
+ .padStart(METRIC_WIDTH)} ${(
35
+ Math.round(YADDUM * pMetric.instability) / YADDUM
36
+ )
37
+ .toString(DECIMAL_BASE)
38
+ .padEnd(METRIC_WIDTH)}`;
39
+ });
40
+ }
41
+
42
+ function metricifyModule({ source, dependents, dependencies, instability }) {
43
+ return {
44
+ name: source,
45
+ moduleCount: 1,
46
+ afferentCouplings: dependents.length,
47
+ efferentCouplings: dependencies.length,
48
+ instability,
49
+ };
50
+ }
51
+
52
+ function metricsAreCalculable(pModule) {
53
+ return (
54
+ !pModule.coreModule &&
55
+ !pModule.couldNotResolve &&
56
+ !pModule.matchesDoNotFollow
57
+ );
58
+ }
59
+
60
+ function orderByInstability(pLeft, pRight) {
61
+ return pRight.instability - pLeft.instability;
62
+ }
63
+ function transformMetricsToTable({ modules, folders }) {
64
+ // TODO: should probably use a table module for this (i.e. text-table)
65
+ // to simplify this code
66
+ const lMaxNameWidth = folders
67
+ .map((pFolder) => pFolder.name.length)
68
+ .concat(modules.map((pModule) => pModule.source.length))
69
+ .concat(COMPONENT_HEADER.length)
70
+ .sort((pLeft, pRight) => pLeft - pRight)
71
+ .pop();
72
+
73
+ return [chalk.bold(getHeader(lMaxNameWidth))]
74
+ .concat(getDemarcationLine(lMaxNameWidth))
75
+ .concat(
76
+ getMetricsTable(
77
+ folders
78
+ .concat(modules.filter(metricsAreCalculable).map(metricifyModule))
79
+ .sort(orderByInstability),
80
+ lMaxNameWidth
81
+ )
82
+ )
83
+ .join(os.EOL)
84
+ .concat(os.EOL);
85
+ }
86
+
87
+ /**
88
+ * Metrics plugin - to test the waters. If we want to use metrics in other
89
+ * reporters - or use e.g. the Ca/ Ce/ I in rules (e.g. to detect violations
90
+ * of Uncle Bob's variable dependency principle)
91
+ *
92
+ * @param {import('../../types/dependency-cruiser').ICruiseResult} pCruiseResult -
93
+ * the output of a dependency-cruise adhering to dependency-cruiser's
94
+ * cruise result schema
95
+ * @return {import('../../types/dependency-cruiser').IReporterOutput} -
96
+ * output: some metrics on folders and dependencies
97
+ * exitCode: 0
98
+ */
99
+ module.exports = (pCruiseResult) => {
100
+ if (pCruiseResult.folders) {
101
+ return {
102
+ output: transformMetricsToTable(pCruiseResult),
103
+ exitCode: 0,
104
+ };
105
+ } else {
106
+ return {
107
+ output:
108
+ `${os.EOL}ERROR: The cruise result didn't contain any metrics - re-running the cruise with${os.EOL}` +
109
+ ` the '--metrics' command line option should fix that.${os.EOL}${os.EOL}`,
110
+ exitCode: 1,
111
+ };
112
+ }
113
+ };
@@ -45,10 +45,26 @@ function reportAllowedRule(pAllowedRule, pViolations) {
45
45
  return lReturnValue;
46
46
  }
47
47
 
48
- function reportViolatedRules(pRuleSetUsed, pViolations) {
48
+ function reportIgnoredRules(pIgnoredCount) {
49
+ let lReturnValue = [];
50
+
51
+ if (pIgnoredCount > 0) {
52
+ lReturnValue = tsm.inspectionType({
53
+ id: "ignored-known-violations",
54
+ name: "ignored-known-violations",
55
+ description:
56
+ "some dependency violations were ignored; run without --ignore-known to see them",
57
+ category: CATEGORY,
58
+ });
59
+ }
60
+ return lReturnValue;
61
+ }
62
+
63
+ function reportViolatedRules(pRuleSetUsed, pViolations, pIgnoredCount) {
49
64
  return reportRules(_get(pRuleSetUsed, "forbidden", []), pViolations)
50
65
  .concat(reportAllowedRule(_get(pRuleSetUsed, "allowed", []), pViolations))
51
- .concat(reportRules(_get(pRuleSetUsed, "required", []), pViolations));
66
+ .concat(reportRules(_get(pRuleSetUsed, "required", []), pViolations))
67
+ .concat(reportIgnoredRules(pIgnoredCount));
52
68
  }
53
69
 
54
70
  function determineTo(pViolation) {
@@ -66,15 +82,31 @@ function bakeViolationMessage(pViolation) {
66
82
  ? pViolation.from
67
83
  : `${pViolation.from} -> ${determineTo(pViolation)}`;
68
84
  }
69
- function reportViolations(pViolations) {
70
- return pViolations.map((pViolation) =>
71
- tsm.inspection({
72
- typeId: pViolation.rule.name,
73
- message: bakeViolationMessage(pViolation),
74
- file: pViolation.from,
75
- SEVERITY: severity2teamcitySeverity(pViolation.rule.severity),
76
- })
77
- );
85
+
86
+ function reportIgnoredViolation(pIgnoredCount) {
87
+ let lReturnValue = [];
88
+
89
+ if (pIgnoredCount > 0) {
90
+ lReturnValue = tsm.inspection({
91
+ typeId: "ignored-known-violations",
92
+ message: `${pIgnoredCount} known violations ignored. Run without --ignore-known to see them.`,
93
+ SEVERITY: "WARNING",
94
+ });
95
+ }
96
+ return lReturnValue;
97
+ }
98
+
99
+ function reportViolations(pViolations, pIgnoredCount) {
100
+ return pViolations
101
+ .map((pViolation) =>
102
+ tsm.inspection({
103
+ typeId: pViolation.rule.name,
104
+ message: bakeViolationMessage(pViolation),
105
+ file: pViolation.from,
106
+ SEVERITY: severity2teamcitySeverity(pViolation.rule.severity),
107
+ })
108
+ )
109
+ .concat(reportIgnoredViolation(pIgnoredCount));
78
110
  }
79
111
 
80
112
  /**
@@ -96,11 +128,14 @@ module.exports = (pResults) => {
96
128
  tsm.stdout = false;
97
129
 
98
130
  const lRuleSet = _get(pResults, "summary.ruleSetUsed", []);
99
- const lViolations = _get(pResults, "summary.violations", []);
131
+ const lViolations = _get(pResults, "summary.violations", []).filter(
132
+ (pViolation) => pViolation.rule.severity !== "ignore"
133
+ );
134
+ const lIgnoredCount = _get(pResults, "summary.ignore", 0);
100
135
 
101
136
  return {
102
- output: reportViolatedRules(lRuleSet, lViolations)
103
- .concat(reportViolations(lViolations))
137
+ output: reportViolatedRules(lRuleSet, lViolations, lIgnoredCount)
138
+ .concat(reportViolations(lViolations, lIgnoredCount))
104
139
  .reduce((pAll, pCurrent) => `${pAll}${pCurrent}\n`, ""),
105
140
  exitCode: pResults.summary.error,
106
141
  };
@@ -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:[^:]+$" },
@@ -4,7 +4,7 @@ const wrapAnsi = require("wrap-ansi");
4
4
  const DEFAULT_INDENT = 4;
5
5
  module.exports = function wrapAndIndent(pString, pIndent = DEFAULT_INDENT) {
6
6
  const lDogmaticMaxConsoleWidth = 78;
7
- const MAX_WIDTH = lDogmaticMaxConsoleWidth - pIndent;
7
+ const lMaxWidth = lDogmaticMaxConsoleWidth - pIndent;
8
8
 
9
- return indentString(wrapAnsi(pString, MAX_WIDTH), pIndent);
9
+ return indentString(wrapAnsi(pString, lMaxWidth), pIndent);
10
10
  };
@@ -52,6 +52,7 @@ function match(pFrom, pTo) {
52
52
  );
53
53
  };
54
54
  }
55
+
55
56
  const isInteresting = (pRule) => !isModuleOnlyRule(pRule);
56
57
 
57
58
  module.exports = {
@@ -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
+ }
@@ -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";