├── .gitignore ├── README.md ├── go.mod ├── go.sum ├── godep └── godep.go ├── main.go ├── screenshots ├── godep.png ├── godep.svg └── godep1.png ├── tmp └── .gitkeep └── util └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=go,visualstudiocode 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | godepviz 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | 21 | tmp/* 22 | !tmp/.gitkeep 23 | 24 | ### Go Patch ### 25 | /vendor/ 26 | /Godeps/ 27 | 28 | ### VisualStudioCode ### 29 | .vscode/* 30 | !.vscode/settings.json 31 | !.vscode/tasks.json 32 | !.vscode/launch.json 33 | !.vscode/extensions.json 34 | 35 | 36 | .idea/* 37 | ### VisualStudioCode Patch ### 38 | # Ignore all local history of files 39 | .history 40 | 41 | # End of https://www.gitignore.io/api/go,visualstudiocode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # godepviz 2 | 3 | Visualize Go dependency in Graphviz DOT format. 4 | 5 | Support visualize package from [https://pkg.go.dev](https://pkg.go.dev/) 6 | 7 | ## Installation 8 | 9 | ```bash 10 | $ go get github.com/chuongtrh/godepviz 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```bash 16 | $ godepviz github.com/gofiber/fiber/v2 17 | ``` 18 | 19 | The output is a graph in [Graphviz](http://graphviz.org/) dot format. 20 | 21 | ```base 22 | digraph G { 23 | rankdir="LR"; 24 | pad=.25; 25 | ratio=fill; 26 | dpi=360; 27 | node [shape=box]; 28 | "github.com/gofiber/fiber/v2" -> "github.com/valyala/fasthttp"; 29 | "github.com/valyala/fasthttp" -> "github.com/andybalholm/brotli"; 30 | "github.com/valyala/fasthttp" -> "github.com/klauspost/compress/flate"; 31 | "github.com/valyala/fasthttp" -> "github.com/klauspost/compress/gzip"; 32 | "github.com/valyala/fasthttp" -> "github.com/klauspost/compress/zlib"; 33 | "github.com/valyala/fasthttp" -> "github.com/valyala/bytebufferpool"; 34 | "github.com/gofiber/fiber/v2" -> "github.com/valyala/fasthttp/reuseport"; 35 | "github.com/valyala/fasthttp/reuseport" -> "github.com/valyala/tcplisten"; 36 | "github.com/gofiber/fiber/v2" [style=filled]; 37 | } 38 | ``` 39 | 40 | You can view a output graph with tool [https://dreampuf.github.io/GraphvizOnline](https://dreampuf.github.io/GraphvizOnline) 41 | or [http://viz-js.com](http://viz-js.com) 42 | 43 | If you have the **graphviz** tools installed you can render it by piping the output to dot: 44 | 45 | ```bash 46 | $ godepviz github.com/gofiber/fiber/v2 | dot -Tpng -o godep.png 47 | ``` 48 | 49 | Export SVG file with a large graph have many nodes of dependencies 50 | 51 | ```bash 52 | $ godepviz github.com/gofiber/fiber/v2 | dot -Gdpi=0 -T svg -o godep.svg 53 | ``` 54 | 55 | Run from source code 56 | 57 | ```bash 58 | $ go run main.go go.uber.org/zap | dot -Tpng -o godep.png 59 | ``` 60 | 61 | ## Example 62 | 63 | - Package `net/http` 64 | 65 | ![Example net/http](./screenshots/godep.png) 66 | 67 | - Package `github.com/labstack/echo/v4` 68 | 69 | ![Example github.com/labstack/echo/v4](./screenshots/godep1.png) 70 | 71 | - Package `github.com/gofiber/fiber/v2` 72 | 73 | ![Example github.com/gofiber/fiber/v2](./screenshots/godep.svg) 74 | 75 | ## License 76 | MIT 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chuongtrh/godepviz 2 | 3 | go 1.15 4 | 5 | require github.com/anaskhan96/soup v1.2.4 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/anaskhan96/soup v1.2.4 h1:or+sKs9QbzJGZVTYFmTs2VBateEywoq00a6K14z331E= 2 | github.com/anaskhan96/soup v1.2.4/go.mod h1:6YnEp9A2yywlYdM4EgDz9NEHclocMepEtku7wg6Cq3s= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 9 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 11 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= 12 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 13 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 14 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 15 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | -------------------------------------------------------------------------------- /godep/godep.go: -------------------------------------------------------------------------------- 1 | package godep 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/anaskhan96/soup" 13 | ) 14 | 15 | // Node struct 16 | type Node struct { 17 | PkgName string `json:"pkgname"` 18 | IsRoot bool `json:"-"` 19 | Parent *Node `json:"parent"` 20 | ImportType string `json:"import_type"` // internal/external/standard 21 | Imports []*Node `json:"imports"` 22 | } 23 | 24 | // FindImports func 25 | func (node *Node) FindImports() error { 26 | 27 | if node.ImportType == "standard" || node.ImportType == "external" { 28 | return nil 29 | } 30 | 31 | pkgImportURL := pkgImportURL(node.PkgName) 32 | imports, err := fetchImport(pkgImportURL, node.IsRoot) 33 | if err != nil && node.IsRoot { 34 | return errors.New("Package " + node.PkgName + " not found") 35 | } 36 | 37 | for key, val := range imports { 38 | pkgName := key 39 | childNode := &Node{ 40 | PkgName: pkgName, 41 | IsRoot: false, 42 | Parent: node, 43 | ImportType: val, 44 | } 45 | childNode.FindImports() 46 | node.Imports = append(node.Imports, childNode) 47 | } 48 | return nil 49 | } 50 | 51 | func (node *Node) graph(existEdges map[string]bool, nodes *[]string, edges *[]string) error { 52 | for _, imp := range node.Imports { 53 | edge := fmt.Sprintf("%s->%s", node.PkgName, imp.PkgName) 54 | // Check existEdges 55 | _, ok := existEdges[edge] 56 | if !ok { 57 | existEdges[edge] = true 58 | edge := fmt.Sprintf(" \"%s\" -> \"%s\";\n", node.PkgName, imp.PkgName) 59 | *edges = append(*edges, edge) 60 | if imp.ImportType == "standard" { 61 | node := fmt.Sprintf(" \"%s\" [style=filled,color=steelblue1];\n", imp.PkgName) 62 | *nodes = append(*nodes, node) 63 | } else if imp.ImportType == "internal" { 64 | node := fmt.Sprintf(" \"%s\" [style=filled,color=chocolate1];\n", imp.PkgName) 65 | *nodes = append(*nodes, node) 66 | } else { 67 | node := fmt.Sprintf(" \"%s\" [style=filled,color=hotpink];\n", imp.PkgName) 68 | *nodes = append(*nodes, node) 69 | } 70 | } 71 | imp.graph(existEdges, nodes, edges) 72 | } 73 | return nil 74 | } 75 | 76 | // BuildGraph func 77 | func (node *Node) BuildGraph() string { 78 | existEdges := make(map[string]bool) 79 | nodes := make([]string, 0) 80 | edges := make([]string, 0) 81 | 82 | buf := bytes.NewBuffer([]byte{}) 83 | buf.WriteString("digraph G {\n") 84 | buf.WriteString(" rankdir=\"LR\";\n") 85 | buf.WriteString(" labelloc=\"t\";\n") 86 | buf.WriteString(" label=\"Package: " + node.PkgName + "\";\n") 87 | buf.WriteString(" pad=.5;\n") 88 | buf.WriteString(" ratio=auto;\n") 89 | buf.WriteString(" dpi=360;\n") 90 | buf.WriteString(" graph [fontsize=16 fontname=\"Roboto Condensed, sans-serif\"];\n") 91 | buf.WriteString(" node [shape=box style=rounded fontname=\"Roboto Condensed, sans-serif\" fontsize=11 height=0 width=0 margin=.08];\n") 92 | buf.WriteString(" edge [fontsize=10, fontname=\"Roboto Condensed, sans-serif\" splines=\"polyline\"];\n") 93 | 94 | node.graph(existEdges, &nodes, &edges) 95 | 96 | sort.Strings(nodes) 97 | sort.Strings(edges) 98 | 99 | buf.WriteString(fmt.Sprintf(" \"%s\" [style=filled,color=palegreen];\n", node.PkgName)) 100 | 101 | buf.WriteString("// Nodes") 102 | for _, node := range nodes { 103 | buf.WriteString(node) 104 | } 105 | 106 | buf.WriteString("// Edges") 107 | for _, edge := range edges { 108 | buf.WriteString(edge) 109 | } 110 | 111 | buf.WriteString("}") 112 | return buf.String() 113 | } 114 | 115 | func pkgURL(pkgName string) string { 116 | return "https://pkg.go.dev/" + pkgName 117 | } 118 | 119 | func pkgImportURL(pkgName string) string { 120 | return "https://pkg.go.dev/" + pkgName + "?tab=imports" 121 | } 122 | 123 | func fetchImport(pkgImportURL string, isRoot bool) (map[string]string, error) { 124 | imports := make(map[string]string) 125 | var htmlContent string 126 | if isRoot { 127 | resp, err := http.Get(pkgImportURL) 128 | if err != nil { 129 | return imports, err 130 | } 131 | defer resp.Body.Close() 132 | if resp.StatusCode == 200 { 133 | htmlDoc, err := ioutil.ReadAll(resp.Body) 134 | if err != nil { 135 | return imports, err 136 | } 137 | htmlContent = string(htmlDoc) 138 | } else { 139 | return imports, errors.New("Not_Found") 140 | } 141 | } else { 142 | resp, err := soup.Get(pkgImportURL) 143 | if err != nil { 144 | fmt.Println("Not Found", pkgImportURL) 145 | return imports, err 146 | } 147 | htmlContent = resp 148 | } 149 | 150 | root := soup.HTMLParse(htmlContent) 151 | 152 | docs := root.FindAll("h2", "class", "Imports-heading") 153 | for _, doc := range docs { 154 | text := strings.ToLower(doc.Text()) 155 | 156 | if strings.Contains(text, "standard library imports") { 157 | temp := doc.FindNextElementSibling() 158 | links := temp.FindAll("a") 159 | for _, link := range links { 160 | imports[link.FullText()] = "standard" 161 | } 162 | } else if strings.Contains(text, "imports in module") { 163 | temp := doc.FindNextElementSibling() 164 | links := temp.FindAll("a") 165 | for _, link := range links { 166 | imports[link.FullText()] = "internal" 167 | } 168 | } else if strings.Contains(text, "imports") { 169 | temp := doc.FindNextElementSibling() 170 | links := temp.FindAll("a") 171 | for _, link := range links { 172 | imports[link.FullText()] = "external" 173 | } 174 | } 175 | } 176 | return imports, nil 177 | } 178 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/chuongtrh/godepviz/godep" 9 | ) 10 | 11 | func main() { 12 | 13 | flag.Parse() 14 | args := flag.Args() 15 | if len(args) < 1 { 16 | log.Fatal("Need package name to process") 17 | } 18 | pkgName := args[0] 19 | node := &godep.Node{ 20 | PkgName: pkgName, 21 | IsRoot: true, 22 | Parent: nil, 23 | } 24 | err := node.FindImports() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | graph := node.BuildGraph() 29 | fmt.Println(graph) 30 | } 31 | -------------------------------------------------------------------------------- /screenshots/godep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chuongtrh/godepviz/0afd86e077fa54f7bf111e9a58118d90362c21d9/screenshots/godep.png -------------------------------------------------------------------------------- /screenshots/godep.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | 13 | 14 | github.com/gofiber/fiber/v2 15 | 16 | github.com/gofiber/fiber/v2 17 | 18 | 19 | 20 | github.com/valyala/fasthttp 21 | 22 | github.com/valyala/fasthttp 23 | 24 | 25 | 26 | github.com/gofiber/fiber/v2->github.com/valyala/fasthttp 27 | 28 | 29 | 30 | 31 | 32 | github.com/valyala/fasthttp/reuseport 33 | 34 | github.com/valyala/fasthttp/reuseport 35 | 36 | 37 | 38 | github.com/gofiber/fiber/v2->github.com/valyala/fasthttp/reuseport 39 | 40 | 41 | 42 | 43 | 44 | github.com/andybalholm/brotli 45 | 46 | github.com/andybalholm/brotli 47 | 48 | 49 | 50 | github.com/valyala/fasthttp->github.com/andybalholm/brotli 51 | 52 | 53 | 54 | 55 | 56 | github.com/klauspost/compress/flate 57 | 58 | github.com/klauspost/compress/flate 59 | 60 | 61 | 62 | github.com/valyala/fasthttp->github.com/klauspost/compress/flate 63 | 64 | 65 | 66 | 67 | 68 | github.com/klauspost/compress/gzip 69 | 70 | github.com/klauspost/compress/gzip 71 | 72 | 73 | 74 | github.com/valyala/fasthttp->github.com/klauspost/compress/gzip 75 | 76 | 77 | 78 | 79 | 80 | github.com/klauspost/compress/zlib 81 | 82 | github.com/klauspost/compress/zlib 83 | 84 | 85 | 86 | github.com/valyala/fasthttp->github.com/klauspost/compress/zlib 87 | 88 | 89 | 90 | 91 | 92 | github.com/valyala/bytebufferpool 93 | 94 | github.com/valyala/bytebufferpool 95 | 96 | 97 | 98 | github.com/valyala/fasthttp->github.com/valyala/bytebufferpool 99 | 100 | 101 | 102 | 103 | 104 | github.com/valyala/tcplisten 105 | 106 | github.com/valyala/tcplisten 107 | 108 | 109 | 110 | github.com/valyala/fasthttp/reuseport->github.com/valyala/tcplisten 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /screenshots/godep1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chuongtrh/godepviz/0afd86e077fa54f7bf111e9a58118d90362c21d9/screenshots/godep1.png -------------------------------------------------------------------------------- /tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "os/exec" 8 | "time" 9 | ) 10 | 11 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 12 | 13 | // RandSeq func 14 | func RandSeq(n int) string { 15 | b := make([]rune, n) 16 | for i := range b { 17 | b[i] = letters[rand.Intn(len(letters))] 18 | } 19 | return string(b) 20 | } 21 | 22 | // RenderImage func 23 | func RenderImage(str string, isSaveImage bool, path string) ([]byte, error) { 24 | 25 | id := fmt.Sprintf("%x", time.Now().Unix()) 26 | dotFile := fmt.Sprintf("%s/%s.dot", path, id) 27 | pngFile := fmt.Sprintf("%s/%s.png", path, id) 28 | 29 | ioutil.WriteFile(dotFile, []byte(str), 0755) 30 | 31 | png, err := exec.Command("dot", "-Tpng", dotFile).Output() 32 | if err != nil { 33 | return []byte{}, err 34 | } 35 | if isSaveImage { 36 | ioutil.WriteFile(pngFile, png, 0755) 37 | } 38 | return png, err 39 | } 40 | --------------------------------------------------------------------------------