dependency-cruiser 10.8.0-beta-1 → 11.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dependency-cruiser",
3
- "version": "10.8.0-beta-1",
3
+ "version": "11.0.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",
@@ -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,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
+ };
@@ -17,7 +17,12 @@ function foldersObject2folderArray(pObject) {
17
17
  }));
18
18
  }
19
19
 
20
+ function shouldDeriveMetrics(pOptions) {
21
+ return pOptions.metrics || pOptions.outputType === "metrics";
22
+ }
23
+
20
24
  module.exports = {
21
25
  getParentFolders,
22
26
  foldersObject2folderArray,
27
+ shouldDeriveMetrics,
23
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,5 +1,5 @@
1
1
  const enrichModules = require("./enrich-modules");
2
- const deriveFolders = require("./derive/folders");
2
+ const deriveFolderMetrics = require("./derive/metrics/folder.js");
3
3
  const summarize = require("./summarize");
4
4
  const clearCaches = require("./clear-caches");
5
5
 
@@ -10,7 +10,7 @@ module.exports = function enrich(pModules, pOptions, pFileAndDirectoryArray) {
10
10
  clearCaches();
11
11
  return {
12
12
  modules: lModules,
13
- ...deriveFolders(lModules, pOptions),
13
+ ...deriveFolderMetrics(lModules, pOptions),
14
14
  summary: summarize(lModules, pOptions, pFileAndDirectoryArray),
15
15
  };
16
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) {
@@ -21,15 +21,18 @@ function extractFromSwcAST(pOptions, pFileName) {
21
21
  );
22
22
  }
23
23
 
24
- function extractFromTypeScriptAST(pOptions, pFileName) {
24
+ function extractFromTypeScriptAST(pOptions, pFileName, pTranspileOptions) {
25
25
  return extractTypeScriptDeps(
26
- toTypescriptAST.getASTCached(path.join(pOptions.baseDir, pFileName)),
26
+ toTypescriptAST.getASTCached(
27
+ path.join(pOptions.baseDir, pFileName),
28
+ pTranspileOptions
29
+ ),
27
30
  pOptions.exoticRequireStrings
28
31
  );
29
32
  }
30
33
 
31
34
  function isTypeScriptCompatible(pFileName) {
32
- return [".ts", ".tsx", ".js", ".mjs", ".cjs"].includes(
35
+ return [".ts", ".tsx", ".js", ".mjs", ".cjs", ".vue"].includes(
33
36
  path.extname(pFileName)
34
37
  );
35
38
  }
@@ -88,9 +91,11 @@ function extractWithTsc(
88
91
  pFileName,
89
92
  pTranspileOptions
90
93
  ) {
91
- pDependencies = extractFromTypeScriptAST(pCruiseOptions, pFileName).filter(
92
- (pDep) => pCruiseOptions.moduleSystems.includes(pDep.moduleSystem)
93
- );
94
+ pDependencies = extractFromTypeScriptAST(
95
+ pCruiseOptions,
96
+ pFileName,
97
+ pTranspileOptions
98
+ ).filter((pDep) => pCruiseOptions.moduleSystems.includes(pDep.moduleSystem));
94
99
 
95
100
  if (pCruiseOptions.tsPreCompilationDeps === "specify") {
96
101
  pDependencies = detectPreCompilationNess(
@@ -2,21 +2,28 @@ const fs = require("fs");
2
2
  const tryRequire = require("semver-try-require");
3
3
  const _memoize = require("lodash/memoize");
4
4
  const { supportedTranspilers } = require("../../../src/meta.js");
5
+ const transpile = require("../transpile");
6
+ const getExtension = require("../utl/get-extension");
5
7
 
6
8
  const typescript = tryRequire("typescript", supportedTranspilers.typescript);
7
9
 
8
10
  /**
9
11
  * Compiles pTypescriptSource into a (typescript) AST
10
12
  *
11
- * @param {string} pTypescriptSource - the source to compile
12
- * @param {string} [pFileName] - (optional) file name the typescript
13
- * compiler can use in error messages
13
+ * @param {object} pFileRecord Record with source code, an extension and a filename
14
+ * @param {any} [pTranspileOptions] options for the transpiler(s) - a tsconfig or
15
+ * a babel config
14
16
  * @return {object} - a (typescript) AST
15
17
  */
16
- function getASTFromSource(pTypescriptSource, pFileName) {
18
+ function getASTFromSource(pFileRecord, pTranspileOptions) {
19
+ let lSource = pFileRecord.source;
20
+ if (pFileRecord.extension === ".vue") {
21
+ lSource = transpile(pFileRecord, pTranspileOptions);
22
+ }
23
+
17
24
  return typescript.createSourceFile(
18
- pFileName || "$internal-file-name",
19
- pTypescriptSource,
25
+ pFileRecord.filename || "$internal-file-name",
26
+ lSource,
20
27
  typescript.ScriptTarget.Latest,
21
28
  false
22
29
  );
@@ -27,10 +34,19 @@ function getASTFromSource(pTypescriptSource, pFileName) {
27
34
  * AST and returns it
28
35
  *
29
36
  * @param {string} pFileName - the name of the file to compile
37
+ * @param {any} [pTranspileOptions] options for the transpiler(s) - a tsconfig or
38
+ * a babel config
30
39
  * @return {object} - a (typescript) AST
31
40
  */
32
- function getAST(pFileName) {
33
- return getASTFromSource(fs.readFileSync(pFileName, "utf8"), pFileName);
41
+ function getAST(pFileName, pTranspileOptions) {
42
+ return getASTFromSource(
43
+ {
44
+ source: fs.readFileSync(pFileName, "utf8"),
45
+ extension: getExtension(pFileName),
46
+ filename: pFileName,
47
+ },
48
+ pTranspileOptions
49
+ );
34
50
  }
35
51
 
36
52
  const getASTCached = _memoize(getAST);
@@ -5,16 +5,20 @@ const path = require("path");
5
5
  *
6
6
  * Just using path.extname would be fine for most cases,
7
7
  * except for coffeescript, where a markdown extension can
8
- * mean literate coffeescript.
8
+ * mean literate coffeescript, and for typescript where
9
+ * .d.ts and .ts are slightly different beasts
9
10
  *
10
11
  * @param {string} pFileName path to the file to be parsed
11
12
  * @return {string} extension
12
13
  */
13
14
  module.exports = function getExtensions(pFileName) {
14
- let lReturnValue = path.extname(pFileName);
15
+ if (pFileName.endsWith(".d.ts")) {
16
+ return ".d.ts";
17
+ }
15
18
 
16
- if (lReturnValue === ".md") {
17
- return pFileName.endsWith(".coffee.md") ? ".coffee.md" : lReturnValue;
19
+ if (pFileName.endsWith(".coffee.md")) {
20
+ return ".coffee.md";
18
21
  }
19
- return lReturnValue;
22
+
23
+ return path.extname(pFileName);
20
24
  };
@@ -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.8.0-beta-1",
4
+ version: "11.0.0",
5
5
  engines: {
6
6
  node: "^12.20||^14||>=16",
7
7
  },
@@ -1,62 +1,110 @@
1
- const os = require("os");
1
+ const { EOL } = require("os");
2
2
  const chalk = require("chalk");
3
3
 
4
4
  const DECIMAL_BASE = 10;
5
5
  const METRIC_WIDTH = 4;
6
6
  const INSTABILITY_DECIMALS = 2;
7
7
  const YADDUM = DECIMAL_BASE ** INSTABILITY_DECIMALS;
8
+ const COMPONENT_HEADER = "name";
8
9
 
9
- function transformMetricsToTable(pMetrics) {
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
+ ) {
10
76
  // TODO: should probably use a table module for this (i.e. text-table)
11
- // to simplify this code; but for this poc not having a dependency (so it's
12
- // copy-n-pasteable from a gist) is more important
13
- const lMaxNameWidth = pMetrics
14
- .map((pMetric) => pMetric.name.length)
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)
15
86
  .sort((pLeft, pRight) => pLeft - pRight)
16
87
  .pop();
17
88
 
18
- return [
19
- chalk.bold(
20
- `${"folder".padEnd(lMaxNameWidth)} ${"N".padStart(
21
- METRIC_WIDTH + 1
22
- )} ${"Ca".padStart(METRIC_WIDTH + 1)} ${"Ce".padStart(
23
- METRIC_WIDTH + 1
24
- )} ${"I".padEnd(METRIC_WIDTH + 1)}`
25
- ),
26
- ]
27
- .concat(
28
- `${"-".repeat(lMaxNameWidth)} ${"-".repeat(
29
- METRIC_WIDTH + 1
30
- )} ${"-".repeat(METRIC_WIDTH + 1)} ${"-".repeat(
31
- METRIC_WIDTH + 1
32
- )} ${"-".repeat(METRIC_WIDTH + 1)}`
33
- )
89
+ return [chalk.bold(getHeader(lMaxNameWidth))]
90
+ .concat(getDemarcationLine(lMaxNameWidth))
34
91
  .concat(
35
- pMetrics.map((pMetric) => {
36
- return `${pMetric.name.padEnd(
37
- lMaxNameWidth,
38
- " "
39
- )} ${pMetric.moduleCount
40
- .toString(DECIMAL_BASE)
41
- .padStart(METRIC_WIDTH)} ${pMetric.afferentCouplings
42
- .toString(DECIMAL_BASE)
43
- .padStart(METRIC_WIDTH)} ${pMetric.efferentCouplings
44
- .toString(DECIMAL_BASE)
45
- .padStart(METRIC_WIDTH)} ${(
46
- Math.round(YADDUM * pMetric.instability) / YADDUM
47
- )
48
- .toString(DECIMAL_BASE)
49
- .padEnd(METRIC_WIDTH)}`;
50
- })
92
+ getMetricsTable(
93
+ lComponents
94
+ .sort(orderByString("name"))
95
+ .sort(orderByNumber(orderBy || "instability")),
96
+ lMaxNameWidth
97
+ )
51
98
  )
52
- .join(os.EOL)
53
- .concat(os.EOL);
99
+ .join(EOL)
100
+ .concat(EOL);
54
101
  }
55
102
 
56
103
  /**
57
- * Metrics plugin - to test the waters. If we want to use metrics in other
58
- * reporters - or use e.g. the Ca/ Ce/ I in rules (e.g. to detect violations
59
- * of Uncle Bob's variable dependency principle)
104
+ * returns stability metrics of modules & folders in an ascii table
105
+ *
106
+ * Potential future features:
107
+ * - additional output formats (csv?, html?)
60
108
  *
61
109
  * @param {import('../../types/dependency-cruiser').ICruiseResult} pCruiseResult -
62
110
  * the output of a dependency-cruise adhering to dependency-cruiser's
@@ -65,17 +113,18 @@ function transformMetricsToTable(pMetrics) {
65
113
  * output: some metrics on folders and dependencies
66
114
  * exitCode: 0
67
115
  */
68
- module.exports = (pCruiseResult) => {
116
+ module.exports = (pCruiseResult, pReporterOptions) => {
117
+ const lReporterOptions = pReporterOptions || {};
69
118
  if (pCruiseResult.folders) {
70
119
  return {
71
- output: transformMetricsToTable(pCruiseResult.folders),
120
+ output: transformMetricsToTable(pCruiseResult, lReporterOptions),
72
121
  exitCode: 0,
73
122
  };
74
123
  } else {
75
124
  return {
76
125
  output:
77
- `${os.EOL}ERROR: The cruise result didn't contain any metrics - re-running the cruise with${os.EOL}` +
78
- ` the '--metrics' command line option should fix that.${os.EOL}${os.EOL}`,
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}`,
79
128
  exitCode: 1,
80
129
  };
81
130
  }
@@ -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,
@@ -47,6 +47,7 @@ module.exports = {
47
47
  items: { $ref: "#/definitions/RuleSummaryType" },
48
48
  },
49
49
  consolidated: { type: "boolean" },
50
+ instability: { type: "number" },
50
51
  },
51
52
  },
52
53
  ReachableType: {
@@ -570,6 +571,7 @@ module.exports = {
570
571
  dot: { $ref: "#/definitions/DotReporterOptionsType" },
571
572
  ddot: { $ref: "#/definitions/DotReporterOptionsType" },
572
573
  flat: { $ref: "#/definitions/DotReporterOptionsType" },
574
+ metrics: { $ref: "#/definitions/MetricsReporterOptionsType" },
573
575
  },
574
576
  },
575
577
  AnonReporterOptionsType: {
@@ -577,6 +579,24 @@ module.exports = {
577
579
  additionalProperties: false,
578
580
  properties: { wordlist: { type: "array", items: { type: "string" } } },
579
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
+ },
580
600
  DotReporterOptionsType: {
581
601
  type: "object",
582
602
  additionalProperties: false,
@@ -1,3 +1,3 @@
1
- import { IViolation } from "./cruise-result";
1
+ import { IViolation } from "./violations";
2
2
 
3
3
  export type IBaselineViolations = IViolation[];
@@ -1,11 +1,8 @@
1
1
  import { ICruiseOptions } from "./options";
2
2
  import { IFlattenedRuleSet } from "./rule-set";
3
- import {
4
- DependencyType,
5
- ModuleSystemType,
6
- SeverityType,
7
- ProtocolType,
8
- } from "./shared-types";
3
+ import { DependencyType, ModuleSystemType, ProtocolType } from "./shared-types";
4
+ import { IViolation } from "./violations";
5
+ import { IRuleSummary } from "./rule-summary";
9
6
 
10
7
  export interface ICruiseResult {
11
8
  /**
@@ -106,6 +103,15 @@ export interface IModule {
106
103
  * purposes - it will not be present after a regular cruise.
107
104
  */
108
105
  consolidated?: boolean;
106
+ /**
107
+ * "number of dependents/ (number of dependents + number of dependencies)
108
+ * A measure for how stable the module is; ranging between 0 (completely
109
+ * stable module) to 1 (completely instable module). Derived from Uncle
110
+ * Bob's instability metric - but applied to a single module instead of
111
+ * to a group of them. This attribute is only present when dependency-cruiser
112
+ * was asked to calculate metrics.
113
+ */
114
+ instability?: number;
109
115
  }
110
116
 
111
117
  export interface IDependency {
@@ -217,27 +223,6 @@ export interface IDependency {
217
223
  valid: boolean;
218
224
  }
219
225
 
220
- /**
221
- * If there was a rule violation (valid === false), this object contains the name of the
222
- * rule and severity of violating it.
223
- */
224
- export interface IRuleSummary {
225
- /**
226
- * The (short, eslint style) name of the violated rule. Typically something like
227
- * 'no-core-punycode' or 'no-outside-deps'.
228
- */
229
- name: string;
230
- /**
231
- * How severe a violation of a rule is. The 'error' severity will make some reporters return
232
- * a non-zero exit code, so if you want e.g. a build to stop when there's a rule violated:
233
- * use that. The absence of the 'ignore' severity here is by design; ignored rules don't
234
- * show up in the output.
235
- *
236
- * Severity to use when a dependency is not in the 'allowed' set of rules. Defaults to 'warn'
237
- */
238
- severity: SeverityType;
239
- }
240
-
241
226
  export interface IReachable {
242
227
  /**
243
228
  * The name of the rule where the reachability was defined
@@ -380,29 +365,6 @@ export interface IWebpackConfig {
380
365
  */
381
366
  export type WebpackEnvType = { [key: string]: any } | string;
382
367
 
383
- export interface IViolation {
384
- /**
385
- * The violated rule
386
- */
387
- rule: IRuleSummary;
388
- /**
389
- * The from part of the dependency this violation is about
390
- */
391
- from: string;
392
- /**
393
- * The to part of the dependency this violation is about
394
- */
395
- to: string;
396
- /**
397
- * The circular path if the violation is about circularity
398
- */
399
- cycle?: string[];
400
- /**
401
- * The path from the from to the to if the violation is transitive
402
- */
403
- via?: string[];
404
- }
405
-
406
368
  export interface IFolder {
407
369
  /**
408
370
  * The name of the folder. FOlder names are normalized to posix (so
@@ -446,3 +408,6 @@ export interface IFolder {
446
408
  */
447
409
  instability?: number;
448
410
  }
411
+
412
+ export * from "./violations";
413
+ export * from "./rule-summary";
@@ -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";
@@ -0,0 +1,23 @@
1
+ import { SeverityType } from "./shared-types";
2
+
3
+ /**
4
+ * If there was a rule violation (valid === false), this object contains the name of the
5
+ * rule and severity of violating it.
6
+ */
7
+
8
+ export interface IRuleSummary {
9
+ /**
10
+ * The (short, eslint style) name of the violated rule. Typically something like
11
+ * 'no-core-punycode' or 'no-outside-deps'.
12
+ */
13
+ name: string;
14
+ /**
15
+ * How severe a violation of a rule is. The 'error' severity will make some reporters return
16
+ * a non-zero exit code, so if you want e.g. a build to stop when there's a rule violated:
17
+ * use that. The absence of the 'ignore' severity here is by design; ignored rules don't
18
+ * show up in the output.
19
+ *
20
+ * Severity to use when a dependency is not in the 'allowed' set of rules. Defaults to 'warn'
21
+ */
22
+ severity: SeverityType;
23
+ }
@@ -0,0 +1,24 @@
1
+ import { IRuleSummary } from "./rule-summary";
2
+
3
+ export interface IViolation {
4
+ /**
5
+ * The violated rule
6
+ */
7
+ rule: IRuleSummary;
8
+ /**
9
+ * The from part of the dependency this violation is about
10
+ */
11
+ from: string;
12
+ /**
13
+ * The to part of the dependency this violation is about
14
+ */
15
+ to: string;
16
+ /**
17
+ * The circular path if the violation is about circularity
18
+ */
19
+ cycle?: string[];
20
+ /**
21
+ * The path from the from to the to if the violation is transitive
22
+ */
23
+ via?: string[];
24
+ }
@@ -1,12 +0,0 @@
1
- const getStabilityMetrics = require("./get-stability-metrics");
2
-
3
- function shouldDeriveFolders(pOptions) {
4
- return pOptions.metrics || pOptions.outputType === "metrics";
5
- }
6
-
7
- module.exports = function deriveFolders(pModules, pOptions) {
8
- if (shouldDeriveFolders(pOptions)) {
9
- return { folders: getStabilityMetrics(pModules) };
10
- }
11
- return {};
12
- };