Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(report): adds basic d2 (https://d2lang.com/) reporter #857

Merged
merged 5 commits into from
Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/depcruise-fmt.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ try {
)
.option(
"-T, --output-type <type>",
"output type; e.g. err, err-html, dot, ddot, archi, flat, baseline or json",
"output type; e.g. err, err-html, dot, ddot, archi, flat, d2, mermaid or json",
"err",
)
.option(
Expand Down
2 changes: 1 addition & 1 deletion bin/dependency-cruise.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ try {
)
.option(
"-T, --output-type <type>",
"output type; e.g. err, err-html, dot, ddot, archi, flat, text or json",
"output type; e.g. err, err-html, dot, ddot, archi, flat, d2, mermaid, text or json",
"err",
)
.option("-m, --metrics", "calculate stability metrics", false)
Expand Down
1 change: 1 addition & 0 deletions doc/assets/d2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
647 changes: 1 addition & 646 deletions doc/assets/filtering/snazzy-focus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
511 changes: 1 addition & 510 deletions doc/assets/flat-report-counter-example.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
816 changes: 1 addition & 815 deletions doc/assets/flat-report-example.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
263 changes: 1 addition & 262 deletions doc/assets/theming/bare.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
263 changes: 1 addition & 262 deletions doc/assets/theming/base.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
263 changes: 1 addition & 262 deletions doc/assets/theming/engineering.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
263 changes: 1 addition & 262 deletions doc/assets/theming/vertical.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 21 additions & 1 deletion doc/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ GitLab support this out of the box in their on-line rendering of markdown.

Both due to limitations in the mermaid format and to the relative newness of this
reporter the graph cannot be (made as) feature rich as those produced by the
`dot` reporters.
`dot` or `d2` reporters.

<details>
<summary>Sample output</summary>
Expand Down Expand Up @@ -259,6 +259,26 @@ style src_main_rule_set_normalize_js fill:lime,color:black

</details>

#### d2

Generates a graph in [d2](https://d2lang.com/) format. D2 is a nice, well thought
out alternative to mermaid. It supports a several layout engines, of which ELK looks
especially pleasing. The current trade-off between D2 and dot is that its graphs
tend to take up more space than the dot ones.

Sample use:

```sh
dependency-cruise src/cache --include-only "^src/cache" -T d2 | d2 --layout elk --scale 1 - > dependencygraph.svg
```

<details>
<summary>Sample output</summary>

![d2 representation of dependency-cruiser's caching feature, with d2 set to use the 'ELK' layout](./assets/d2.svg)]

</details>

#### err-html

Generates a stand-alone html report with:
Expand Down
161 changes: 161 additions & 0 deletions src/report/d2.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { EOL } from "node:os";
import { basename, dirname } from "node:path";
import { getURLForModule } from "./utl/index.mjs";

const severity2color = new Map([
["error", "red"],
["warn", "orange"],
["info", "blue"],
]);

/**
* @param {import("../../types/rule-summary").IRuleSummary} pRules
* @returns {string}
*/
function getMaxSeverity(pRules) {
const lSeverity2Number = new Map([
// eslint-disable-next-line no-magic-numbers
["error", 3],
// eslint-disable-next-line no-magic-numbers
["warn", 2],
["info", 1],
]);
return pRules
.map((pRule) => pRule.severity)
.reduce((pMax, pCurrent) => {
return (lSeverity2Number.get(pMax) ?? 0) >
(lSeverity2Number.get(pCurrent) ?? 0)
? pMax
: pCurrent;
});
}

/**
* @param {string} pSource
* @return {string}
*/
function getVertexName(pSource) {
const lFolderName = dirname(pSource)
.split("/")
.map((pPart) => `"${pPart}"`)
.join(".");
const lBaseName = `"${basename(pSource)}"`;
if (lFolderName === `"."`) {
return lBaseName;
}
return `${lFolderName}.${lBaseName}`;
}

/**
* @param {import("../../types/cruise-result").IModule} pModule
* @param {import("../../types/cruise-result").IOptions} pOptions
* @returns {string}
*/
// eslint-disable-next-line complexity
function getModuleAttributes(pModule, pOptions) {
let lReturnValue = "class: module";
if (pModule.consolidated) {
lReturnValue = `${lReturnValue}; style.multiple: true`;
}
if (
pModule.matchesFocus ||
pModule.matchesHighlight ||
pModule.matchesReaches
) {
lReturnValue = `${lReturnValue}; style.fill: yellow`;
}
if (pModule.valid === false) {
lReturnValue = `${lReturnValue}; style.stroke: ${
severity2color.get(getMaxSeverity(pModule.rules)) ?? "orange"
}`;
lReturnValue = `${lReturnValue}; tooltip: "${pModule.rules
.map((pRule) => pRule.name)
.join("\\n")}"`;
}
if (
pModule.dependencyTypes?.some((pDependencyType) =>
pDependencyType.includes("npm"),
)
) {
lReturnValue = `${lReturnValue}; shape: package`;
}
lReturnValue = `${lReturnValue}; link: "${getURLForModule(
pModule,
pOptions?.prefix,
)}"`;
return lReturnValue;
}

/**
* @param {import("../../types/cruise-result").IDependency} pDependency
* @returns {string}
*/
// eslint-disable-next-line complexity
function getDependencyAttributes(pDependency) {
let lThing = "";
if (pDependency.valid === false) {
lThing =
`style: {stroke: ${
severity2color.get(getMaxSeverity(pDependency.rules)) ?? "orange"
}}; ` +
`label: "${pDependency.rules.map((pRule) => pRule.name).join("\\n")}"`;
}
if (pDependency.circular) {
lThing = `${
lThing ? `${lThing};` : lThing
} target-arrowhead: {shape: circle}`;
}
if (pDependency.dynamic) {
lThing = `${
lThing ? `${lThing};` : lThing
} target-arrowhead: {shape: arrow}`;
}
return lThing ? `: {${lThing}}` : lThing;
}

/**
* @param {import('../../types/cruise-result').ICruiseResult} pCruiseResult
* @return {string}
*/
function renderD2Source(pCruiseResult) {
const lVertices = pCruiseResult.modules
.map((pModule) => {
return `${getVertexName(pModule.source)}: {${getModuleAttributes(
pModule,
pCruiseResult.summary.optionsUsed,
)}}`;
})
.join(EOL);
const lEdges = pCruiseResult.modules
.flatMap((pModule) => {
return pModule.dependencies.map((pDependency) => {
return `${getVertexName(pModule.source)} -> ${getVertexName(
pDependency.resolved,
)}${getDependencyAttributes(pDependency)}`;
});
})
.join(EOL);
const lStyles = `classes: {
module: {
height: 30;
style.border-radius: 10;
}
}`;
return (
`# modules${EOL}${EOL}${lVertices}${EOL}${EOL}` +
`# dependencies${EOL}${EOL}${lEdges}${EOL}${EOL}` +
`# styling${EOL}${EOL}${lStyles}${EOL}`
);
}
/**
* d2 reporter
*
* @param {import('../../types/dependency-cruiser').ICruiseResult} pCruiseResult
* @return {import('../../types/dependency-cruiser').IReporterOutput}
*/
export default function d2(pCruiseResult) {
return {
output: renderD2Source(pCruiseResult),
exitCode: 0,
};
}
23 changes: 12 additions & 11 deletions src/report/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@ import { getExternalPluginReporter } from "./plugins.mjs";

const TYPE2MODULE = new Map([
["anon", "./anon/index.mjs"],
["archi", "./dot/dot-custom.mjs"],
["azure-devops", "./azure-devops.mjs"],
["baseline", "./baseline.mjs"],
["cdot", "./dot/dot-custom.mjs"],
["csv", "./csv.mjs"],
["dot", "./dot/dot-module.mjs"],
["d2", "./d2.mjs"],
["ddot", "./dot/dot-folder.mjs"],
["cdot", "./dot/dot-custom.mjs"],
["archi", "./dot/dot-custom.mjs"],
["fdot", "./dot/dot-flat.mjs"],
["flat", "./dot/dot-flat.mjs"],
["dot", "./dot/dot-module.mjs"],
["err-html", "./error-html/index.mjs"],
["markdown", "./markdown.mjs"],
["err-long", "./error-long.mjs"],
["err", "./error.mjs"],
["fdot", "./dot/dot-flat.mjs"],
["flat", "./dot/dot-flat.mjs"],
["html", "./html/index.mjs"],
["json", "./json.mjs"],
["teamcity", "./teamcity.mjs"],
["text", "./text.mjs"],
["baseline", "./baseline.mjs"],
["metrics", "./metrics.mjs"],
["markdown", "./markdown.mjs"],
["mermaid", "./mermaid.mjs"],
["metrics", "./metrics.mjs"],
["null", "./null.mjs"],
["azure-devops", "./azure-devops.mjs"],
["teamcity", "./teamcity.mjs"],
["text", "./text.mjs"],
]);

/**
Expand Down
16 changes: 16 additions & 0 deletions test/report/d2/__fixtures__/00-empty.d2
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# modules



# dependencies



# styling

classes: {
module: {
height: 30;
style.border-radius: 10;
}
}
16 changes: 16 additions & 0 deletions test/report/d2/__fixtures__/01-one-module-no-dependencies.d2
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# modules

"aap"."noot"."mies.js": {class: module; link: "aap/noot/mies.js"}

# dependencies



# styling

classes: {
module: {
height: 30;
style.border-radius: 10;
}
}
16 changes: 16 additions & 0 deletions test/report/d2/__fixtures__/02-one-module-invalid-error.d2
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# modules

"aap"."noot"."mies.js": {class: module; style.stroke: red; tooltip: "no-orphans"; link: "aap/noot/mies.js"}

# dependencies



# styling

classes: {
module: {
height: 30;
style.border-radius: 10;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# modules

"aap"."noot"."mies.js": {class: module; style.stroke: orange; tooltip: "ignored-rule\nno-orphans\nsome-other-rule\nsome-other-rule-again"; link: "aap/noot/mies.js"}

# dependencies



# styling

classes: {
module: {
height: 30;
style.border-radius: 10;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# modules

"aap"."noot"."mies.js": {class: module; style.stroke: orange; tooltip: "yolo-rule"; link: "aap/noot/mies.js"}

# dependencies



# styling

classes: {
module: {
height: 30;
style.border-radius: 10;
}
}
16 changes: 16 additions & 0 deletions test/report/d2/__fixtures__/05-one-module-in-root.d2
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# modules

"mies.js": {class: module; link: "mies.js"}

# dependencies



# styling

classes: {
module: {
height: 30;
style.border-radius: 10;
}
}
16 changes: 16 additions & 0 deletions test/report/d2/__fixtures__/06-one-module-npm.d2
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# modules

"node_modules"."yudelyo"."index.js": {class: module; shape: package; link: "https://www.npmjs.com/package/yudelyo"}

# dependencies



# styling

classes: {
module: {
height: 30;
style.border-radius: 10;
}
}
16 changes: 16 additions & 0 deletions test/report/d2/__fixtures__/07-one-module-matches-highlight.d2
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# modules

"aap"."noot"."mies.js": {class: module; style.fill: yellow; link: "aap/noot/mies.js"}

# dependencies



# styling

classes: {
module: {
height: 30;
style.border-radius: 10;
}
}
Loading
Loading