Skip to content

Commit 360210f

Browse files
authored
Adds support for the d2 syntax (#17)
* Fixes issue where nodes were not added to existing cluster * Adds missing dependency * Adds support for the d2 syntax * Adds d2 to README * Update README.md * Update README.md
1 parent 7faa68a commit 360210f

22 files changed

+403
-58
lines changed

.swiftpm/xcode/xcshareddata/xcschemes/DependencyGraph.xcscheme

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,20 @@
790790
ReferencedContainer = "container:">
791791
</BuildableReference>
792792
</BuildActionEntry>
793+
<BuildActionEntry
794+
buildForTesting = "YES"
795+
buildForRunning = "YES"
796+
buildForProfiling = "YES"
797+
buildForArchiving = "YES"
798+
buildForAnalyzing = "YES">
799+
<BuildableReference
800+
BuildableIdentifier = "primary"
801+
BlueprintIdentifier = "D2GraphMapper"
802+
BuildableName = "D2GraphMapper"
803+
BlueprintName = "D2GraphMapper"
804+
ReferencedContainer = "container:">
805+
</BuildableReference>
806+
</BuildActionEntry>
793807
</BuildActionEntries>
794808
</BuildAction>
795809
<TestAction
@@ -998,6 +1012,26 @@
9981012
ReferencedContainer = "container:">
9991013
</BuildableReference>
10001014
</TestableReference>
1015+
<TestableReference
1016+
skipped = "NO">
1017+
<BuildableReference
1018+
BuildableIdentifier = "primary"
1019+
BlueprintIdentifier = "DirectedGraphTests"
1020+
BuildableName = "DirectedGraphTests"
1021+
BlueprintName = "DirectedGraphTests"
1022+
ReferencedContainer = "container:">
1023+
</BuildableReference>
1024+
</TestableReference>
1025+
<TestableReference
1026+
skipped = "NO">
1027+
<BuildableReference
1028+
BuildableIdentifier = "primary"
1029+
BlueprintIdentifier = "D2GraphMapperTests"
1030+
BuildableName = "D2GraphMapperTests"
1031+
BlueprintName = "D2GraphMapperTests"
1032+
ReferencedContainer = "container:">
1033+
</BuildableReference>
1034+
</TestableReference>
10011035
</Testables>
10021036
</TestAction>
10031037
<LaunchAction
@@ -1021,6 +1055,16 @@
10211055
ReferencedContainer = "container:">
10221056
</BuildableReference>
10231057
</BuildableProductRunnable>
1058+
<CommandLineArguments>
1059+
<CommandLineArgument
1060+
argument = "/Users/simonbs/Developer/RunestoneEditor"
1061+
isEnabled = "YES">
1062+
</CommandLineArgument>
1063+
<CommandLineArgument
1064+
argument = "--syntax d2"
1065+
isEnabled = "YES">
1066+
</CommandLineArgument>
1067+
</CommandLineArguments>
10241068
</LaunchAction>
10251069
<ProfileAction
10261070
buildConfiguration = "Release"

Package.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ let package = Package(
1616
targets: [
1717
.executableTarget(name: "Main", dependencies: [
1818
.product(name: "ArgumentParser", package: "swift-argument-parser"),
19+
"D2GraphMapper",
1920
"DirectedGraphMapper",
2021
"DirectedGraphWriter",
2122
"DOTGraphMapper",
@@ -55,6 +56,11 @@ let package = Package(
5556
], path: "Sources/Library/Commands/GraphCommand"),
5657

5758
// Sources/Library/Graphing
59+
.target(name: "D2GraphMapper", dependencies: [
60+
"DirectedGraph",
61+
"DirectedGraphMapper",
62+
"StringIndentHelpers"
63+
], path: "Sources/Library/Graphing/D2GraphMapper"),
5864
.target(name: "DirectedGraph", path: "Sources/Library/Graphing/DirectedGraph"),
5965
.target(name: "DirectedGraphXcodeHelpers", dependencies: [
6066
"DirectedGraph"
@@ -163,6 +169,10 @@ let package = Package(
163169
.target(name: "StringIndentHelpers", path: "Sources/Library/Utilities/StringIndentHelpers"),
164170

165171
// Tests
172+
.testTarget(name: "D2GraphMapperTests", dependencies: [
173+
"DirectedGraph",
174+
"D2GraphMapper"
175+
]),
166176
.testTarget(name: "DirectedGraphTests", dependencies: [
167177
"DirectedGraph"
168178
]),

README.md

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44

55
dependency-graph is a command-line tool that can visualize the dependencies of packages. The tool takes the path to an Xcode project or a Package.swift file as input and outputs a graph that shows the dependencies of the packages in the project or package.
66

7-
## 👀 Sample
7+
## 👀 Examples
88

9-
The graph below shows the relationship of the products and targets in this package as of December 11, 2022. Click on the image to see a larger version.
9+
The following graphs are examples of the graphs that dependency-graph can output. The first graph built by providing dependency-graph the path to a Package.swift file and the second graph was made by providing dependency-graph the path to an .xcodeproj file as input.
1010

11-
<img width="400" src="./sample-swift-package.png" alt="Example graph showing the dependencies of this package." />
11+
|Swift Package|Xcode Project|
12+
|-|-|
13+
|<img width="400" src="./example-swift-package.png" alt="Example graph showing the dependencies of this package." />|<img width="400" src="./example-xcodeproj.png" alt="Example graph showing the dependencies of an Xcode project." />|
1214

13-
Nodes shaped as an ellipse represent products, e.g. the libraries in a Swift package, and the square nodes represent targets.
15+
Nodes shaped as ellipsis represent products, e.g. the libraries in a Swift package, and the square nodes represent targets.
1416

1517
## 🚀 Getting Started
1618

17-
Start off by installing the tool.
18-
19-
#### Using [Homebrew](https://brew.sh)
19+
Start off by installing the tool with [Homebrew](https://brew.sh).
2020

2121
```bash
2222
brew tap simonbs/dependency-graph https://github.com/simonbs/dependency-graph.git
@@ -39,14 +39,6 @@ brew install dependency-graph
3939
> arch -arm64 brew install dependency-graph
4040
> ```
4141
42-
#### Using [Mint](https://github.com/yonaskolb/Mint)
43-
44-
```bash
45-
mint install simonbs/dependency-graph
46-
```
47-
48-
#### Confirm Installation
49-
5042
You may now run the following command to verify that the tool was installed correctly. The following command should print information on how the tool can be used.
5143
5244
```
@@ -97,7 +89,13 @@ digraph g {
9789
}
9890
```
9991

100-
The output can be rendered to an image by piping it to the [dot CLI](https://graphviz.org/doc/info/command.html), which is part of [Graphviz](https://graphviz.org).
92+
The output can be rendered to an image by piping it to a renderer. See the following sections for details on the supported renderers.
93+
94+
#### DOT
95+
96+
<img width="400" src="./example-dot.png" alt="Example graph rendered with dot." />
97+
98+
By default dependency-graph will use the DOT syntax which can be rendered by the [dot CLI](https://graphviz.org/doc/info/command.html), which is part of [Graphviz](https://graphviz.org).
10199

102100
Install Graphviz and run `dependency-graph` and pass the output to the newly installed `dot` CLI.
103101

@@ -106,21 +104,35 @@ brew install graphviz
106104
dependency-graph ~/Developer/Example | dot -Tsvg -o graph.svg
107105
```
108106

109-
The previous example output would look different but similar when using [the Mermaid diagram syntax](https://mermaid-js.github.io/mermaid/#/flowchart) instead. The syntax is used by passing the `--syntax mermaid` option.
107+
When rendering the graph to a PNG, you will likely want to specify the size of the output to ensure it is readable. To generate an image with dot that is exactly 6000 pixels wide or 8000 pixels tall but not necessarily both, do the following:
108+
109+
```bash
110+
dependency-graph ~/Developer/Example | dot -Tpng -Gsize=60,80\! -Gdpi=100 -o graph.png
111+
```
110112

111-
Output in the Mermaid diagram syntax can be rendered to an image using the [the mermaid cli](https://github.com/mermaid-js/mermaid-cli).
113+
You may want to play around with the values for `--node-spacing` and `--rank-spacing` to increase the readability of the graph.
112114

113115
```bash
114-
npm install -g @mermaid-js/mermaid-cli
115-
dependency-graph --syntax mermaid ~/Developer/Example | mmdc -o graph.svg
116+
dependency-graph --node-spacing 50 --rank-spacing 150 ~/Developer/Example | dot -Tsvg -o graph.svg
116117
```
117118

118-
When rendering the graph to a PNG, you will likely want to specify the size of the output to ensure it is readable. You can do this with both the dot and mermaid CLIs.
119+
For large projects the graph may become unreadable. Passing the output through Grahpviz' [unflatten](https://graphviz.org/docs/cli/unflatten/) command may improve the results.
119120

120-
To generate an image with dot that is exactly 6000 pixels wide or 8000 pixels tall but not necessarily both, do the following:
121+
```bash
122+
dependency-graph ~/Developer/Example | unflatten -l 100 -c 100 -f | dot -Tpng -o graph.png
123+
```
124+
125+
#### Mermaid
126+
127+
<img width="400" src="./example-mermaid.png" alt="Example graph rendered with mermaid." />
128+
129+
Specify the `--syntax mermaid` option to have dependency-graph output a graph using [the Mermaid diagram syntax](https://mermaid-js.github.io/mermaid/#/flowchart).
130+
131+
The output be rendered to an image using the [the mermaid cli](https://github.com/mermaid-js/mermaid-cli).
121132

122133
```bash
123-
dependency-graph ~/Developer/norlys-ios/Features/Notes | dot -Tpng -Gsize=60,80\! -Gdpi=100 -o ~/Desktop/dot.png
134+
npm install -g @mermaid-js/mermaid-cli
135+
dependency-graph --syntax mermaid ~/Developer/Example | mmdc -o graph.svg
124136
```
125137

126138
To generate an image on a page that is 6000 pixels wide with mermaid, do the following:
@@ -135,16 +147,25 @@ You may also want to play around with the values for `--node-spacing` and `--ran
135147
dependency-graph --syntax mermaid --node-spacing 50 --rank-spacing 150 ~/Developer/Example | mmdc -o graph.png
136148
```
137149

138-
Pass the `--packages-only` flag to include only the Xcode project and Swift packages in the graph. This omits the libraries and targets within the Xcode project and Swift packages.
150+
#### D2
139151

140-
<img width="400" src="./sample-packages-only.png" alt="Example graph showing only an Xcode project and Swift packages." />
152+
<img width="400" src="./example-d2.png" alt="Example graph rendered with d2." />
141153

142-
For large projects the graph may become unreadable. Passing the output through Grahpviz' [unflatten](https://graphviz.org/docs/cli/unflatten/) command may improve the resutls.
154+
Specify the `--syntax d2` option to have dependency-graph output a graph using [the d2 scripting language](https://d2lang.com/tour/intro).
155+
156+
The output be rendered to an image using the [the d2 cli](https://github.com/terrastruct/d2#install).
143157

144158
```bash
145-
dependency-graph ~/Developer/Example | unflatten -l 100 -c 100 -f | dot -Tpng -o graph.png
159+
curl -fsSL https://d2lang.com/install.sh | sh -s --
160+
dependency-graph --syntax d2 ~/Developer/Example | d2 - graph.png
146161
```
147162

163+
## Graphing Packages Only
164+
165+
Pass the `--packages-only` flag to include only the Xcode project and Swift packages in the graph. This omits the libraries and targets within the Xcode project and Swift packages.
166+
167+
<img width="400" src="./example-packages-only.png" alt="Example graph showing only an Xcode project and Swift packages." />
168+
148169
## 🤷‍♂️ OK, why?
149170

150171
As I'm splitting my iOS and macOS applications into small Swift packages with several small targets, I started wishing for a way to visualise the relationship between the products and targets in my Swift packages. That's why I built this tool.
@@ -154,9 +175,7 @@ Several other tools can visualise a Swift package, however, I wanted a tool that
154175
The example in the top of this README shows a visualization of a Swift package and the graph below shows a visualisation of an Xcode project.
155176
Notice that the left-most subgraph represents an Xcode project named ScriptUIEditor.xcodeproj and it has three targets: ScriptUIEditor, ScriptBrowserFeature, and ScriptBrowserFeatureUITests. Two of these depends on the Swift packages represented by the remaining subgraphs.
156177

157-
These graphs provide a good way to get an overview of a package or the relationship between several packages. Sometimes it can be helpful to generate multiple graphs to get a good overview, for example, a graph of the entire project and graphs of selected packages. Fortunately, the `dependency-graph` cLI makes this easy as it can take either an Xcode project and a Package.swift file as input.
158-
159-
<img width="400" src="./sample-xcodeproj.png" alt="Example graph showing the dependencies of an Xcode project." />
178+
These graphs provide a good way to get an overview of a package or the relationship between several packages. Sometimes it can be helpful to generate multiple graphs to get a good overview, for example, a graph of the entire project and graphs of selected packages. Fortunately, the `dependency-graph` CLI makes this easy as it can take either an Xcode project and a Package.swift file as input.
160179

161180
## 🧐 ...but how?
162181

Sources/Library/Commands/GraphCommand/DirectedGraphWriterFactory.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import DirectedGraphWriter
22

33
public struct DirectedGraphWriterFactory {
4+
private let d2GraphWriter: any DirectedGraphWriter
45
private let dotGraphWriter: any DirectedGraphWriter
56
private let mermaidGraphWriter: any DirectedGraphWriter
67

7-
public init(dotGraphWriter: any DirectedGraphWriter, mermaidGraphWriter: any DirectedGraphWriter) {
8+
public init(d2GraphWriter: any DirectedGraphWriter, dotGraphWriter: any DirectedGraphWriter, mermaidGraphWriter: any DirectedGraphWriter) {
9+
self.d2GraphWriter = d2GraphWriter
810
self.dotGraphWriter = dotGraphWriter
911
self.mermaidGraphWriter = mermaidGraphWriter
1012
}
1113

1214
func writer(for syntax: Syntax) -> any DirectedGraphWriter {
1315
switch syntax {
16+
case .d2:
17+
return d2GraphWriter
1418
case .dot:
1519
return dotGraphWriter
1620
case .mermaid:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
public enum Syntax: String {
2+
case d2
23
case dot
34
case mermaid
45
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import DirectedGraph
2+
import DirectedGraphMapper
3+
import Foundation
4+
import StringIndentHelpers
5+
6+
enum D2GraphMapperError: LocalizedError {
7+
case failedFindingClusterContainingNode(DirectedGraph.Node)
8+
9+
var errorDescription: String? {
10+
switch self {
11+
case .failedFindingClusterContainingNode(let node):
12+
return "Could not find cluster containing node named '\(node.name)'"
13+
}
14+
}
15+
}
16+
17+
public struct D2GraphMapper: DirectedGraphMapper {
18+
private let settings = D2GraphSettings()
19+
20+
public init() {}
21+
22+
public func map(_ graph: DirectedGraph) throws -> String {
23+
return try graph.stringRepresentation(withSettings: settings)
24+
}
25+
}
26+
27+
extension DirectedGraph {
28+
func stringRepresentation(withSettings settings: D2GraphSettings) throws -> String {
29+
var lines: [String] = []
30+
lines.append(settings.stringRepresentation)
31+
if !clusters.isEmpty {
32+
lines.append(clusters.stringRepresentation)
33+
}
34+
if !nodes.isEmpty {
35+
lines.append(nodes.stringRepresentation)
36+
}
37+
if !edges.isEmpty {
38+
lines.append(try edges.stringRepresentation(in: self))
39+
}
40+
return lines.joined(separator: "\n\n")
41+
}
42+
}
43+
44+
extension DirectedGraph.Cluster {
45+
var stringRepresentation: String {
46+
var lines = ["\(name): \(label) {"]
47+
lines += nodes.map(\.stringRepresentation).indented
48+
lines += ["}"]
49+
return lines.joined(separator: "\n")
50+
}
51+
}
52+
53+
extension DirectedGraph.Node {
54+
var stringRepresentation: String {
55+
var lines = [name + ": " + label]
56+
switch shape {
57+
case .box:
58+
lines += [name + ".shape: rectangle"]
59+
case .ellipse:
60+
lines += [name + ".shape: oval"]
61+
}
62+
return lines.joined(separator: "\n")
63+
}
64+
}
65+
66+
extension DirectedGraph.Edge {
67+
func stringRepresentation(in graph: DirectedGraph) throws -> String {
68+
func path(for node: DirectedGraph.Node) throws -> String {
69+
guard !graph.isRootNode(node) else {
70+
return node.name
71+
}
72+
guard let cluster = graph.cluster(containing: node) else {
73+
throw D2GraphMapperError.failedFindingClusterContainingNode(node)
74+
}
75+
// print(cluster.name + "." + node.name)
76+
return cluster.name + "." + node.name
77+
}
78+
return "\(try path(for: sourceNode)) -> \(try path(for: destinationNode))"
79+
}
80+
}
81+
82+
extension Array where Element == DirectedGraph.Cluster {
83+
var stringRepresentation: String {
84+
return map { $0.stringRepresentation }.joined(separator: "\n\n")
85+
}
86+
}
87+
88+
extension Array where Element == DirectedGraph.Node {
89+
var stringRepresentation: String {
90+
return map(\.stringRepresentation).joined(separator: "\n")
91+
}
92+
}
93+
94+
extension Array where Element == DirectedGraph.Edge {
95+
func stringRepresentation(in graph: DirectedGraph) throws -> String {
96+
return try map { try $0.stringRepresentation(in: graph) }.joined(separator: "\n")
97+
}
98+
}
99+
100+
private extension DirectedGraph {
101+
func isRootNode(_ node: DirectedGraph.Node) -> Bool {
102+
return nodes.contains(node)
103+
}
104+
105+
func cluster(containing node: DirectedGraph.Node) -> DirectedGraph.Cluster? {
106+
return clusters.first { cluster in
107+
return cluster.nodes.contains { $0.name == node.name }
108+
}
109+
}
110+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Foundation
2+
3+
public struct D2GraphSettings {
4+
public let direction: String
5+
6+
public init() {
7+
direction = "right"
8+
}
9+
}
10+
11+
extension D2GraphSettings {
12+
var stringRepresentation: String {
13+
let lines = ["direction: \(direction)"]
14+
return lines.joined(separator: "\n")
15+
}
16+
}

0 commit comments

Comments
 (0)