feat: 新增模块级依赖分析功能

This commit is contained in:
hailaz
2026-01-09 19:31:26 +08:00
parent 51f6b6db86
commit dd1dba383f
9 changed files with 1078 additions and 150 deletions

View File

@ -39,13 +39,13 @@ gf dep -r
gf dep -i=false
gf dep -e
gf dep -e -i=false
gf dep -m
gf dep -m -e
gf dep -M
gf dep -M -D
gf dep -s
gf dep -s -p 8080
gf dep ./internal/... -f tree -d 2
gf dep --external --group -f mermaid
gf dep --main --external -f json
gf dep --module --direct -f json
`
)
@ -62,12 +62,13 @@ type Input struct {
Package string `name:"PACKAGE" arg:"true" brief:"package path to analyze, default is ./..." d:"./..."`
Format string `name:"format" short:"f" brief:"output format: tree/list/mermaid/dot/json" d:"tree"`
Depth int `name:"depth" short:"d" brief:"dependency depth limit, 0 means unlimited" d:"3"`
Group bool `name:"group" short:"g" brief:"group by top-level directory" d:"false"`
Internal bool `name:"internal" short:"i" brief:"show only internal packages" d:"true"`
External bool `name:"external" short:"e" brief:"show external packages" d:"false"`
MainOnly bool `name:"main" short:"m" brief:"analyze only main module packages (exclude submodules)" d:"false"`
NoStd bool `name:"nostd" short:"n" brief:"exclude standard library" d:"true"`
Reverse bool `name:"reverse" short:"r" brief:"show reverse dependencies" d:"false"`
Group bool `name:"group" short:"g" brief:"group by top-level directory" d:"false" orphan:"true"`
Internal bool `name:"internal" short:"i" brief:"show only internal packages" d:"true" orphan:"true"`
External bool `name:"external" short:"e" brief:"show external packages" d:"false" orphan:"true"`
Module bool `name:"module" short:"M" brief:"show module-level dependencies (from go.mod)" d:"false" orphan:"true"`
Direct bool `name:"direct" short:"D" brief:"show only direct dependencies (requires --module)" d:"false" orphan:"true"`
NoStd bool `name:"nostd" short:"n" brief:"exclude standard library" d:"true" orphan:"true"`
Reverse bool `name:"reverse" short:"r" brief:"show reverse dependencies" d:"false" orphan:"true"`
Serve bool `name:"serve" short:"s" brief:"start HTTP server to view dependencies" d:"false" orphan:"true"`
Port int `name:"port" short:"p" brief:"HTTP server port" d:"8888"`
}
@ -82,7 +83,28 @@ func (c cDep) Index(ctx context.Context, in Input) (out *Output, err error) {
// Detect module prefix from go.mod
analyzer.modulePrefix = analyzer.detectModulePrefix()
// Get package information
// Module-level analysis mode (uses go mod graph)
if in.Module {
if in.Serve {
// Load module graph for server mode
if err := analyzer.loadModuleGraph(ctx); err != nil {
mlog.Print("Warning: Failed to load module graph: " + err.Error())
}
return nil, analyzer.startServer(in)
}
// Load module graph
if err := analyzer.loadModuleGraph(ctx); err != nil {
return nil, err
}
// Generate module-level output
output := analyzer.generateModuleOutput(in)
mlog.Print(output)
return
}
// Package-level analysis mode (original behavior)
loadErr := analyzer.loadPackages(ctx, in.Package)
// Start HTTP server if requested

View File

@ -49,31 +49,29 @@ const (
// It consolidates package information from go list output with additional
// metadata for filtering and traversal.
type PackageInfo struct {
ImportPath string // Full import path (e.g., github.com/gogf/gf/v2/os/gfile)
ModulePath string // Module path (e.g., github.com/gogf/gf/v2)
Kind PackageKind // Package classification (Internal/External/StdLib)
Tier int // Package tier: 0=module root, 1=top-level, 2+=nested
Imports []string // Direct imports of this package
IsStdLib bool // Standard library marker (from go list)
IsModuleRoot bool // Is this the root package of its module
IsMainModule bool // Is this package from the main module (not a submodule)
ImportPath string // Full import path (e.g., github.com/gogf/gf/v2/os/gfile)
ModulePath string // Module path (e.g., github.com/gogf/gf/v2)
Kind PackageKind // Package classification (Internal/External/StdLib)
Tier int // Package tier: 0=module root, 1=top-level, 2+=nested
Imports []string // Direct imports of this package
IsStdLib bool // Standard library marker (from go list)
IsModuleRoot bool // Is this the root package of its module
}
// FilterOptions represents filtering criteria for dependency analysis.
// It provides a clear, normalized representation of user filtering preferences.
// Usage:
// opts := &FilterOptions{
// IncludeInternal: true,
// IncludeExternal: false,
// IncludeStdLib: false,
// MainModuleOnly: false,
// Depth: 3,
// }
//
// opts := &FilterOptions{
// IncludeInternal: true,
// IncludeExternal: false,
// IncludeStdLib: false,
// Depth: 3,
// }
type FilterOptions struct {
IncludeInternal bool // Include internal packages from main module
IncludeExternal bool // Include external dependencies
IncludeStdLib bool // Include standard library packages
MainModuleOnly bool // Only analyze packages from main module (exclude submodules)
Depth int // Maximum traversal depth (0 = unlimited)
}
@ -106,15 +104,30 @@ type analyzer struct {
visited map[string]bool
edges map[string]bool
store *PackageStore // New unified package store
// Module-level dependency data (from go mod graph)
modules map[string]*ModuleInfo // All modules indexed by path
moduleGraph map[string][]string // Module dependency graph: module -> dependencies
directModules map[string]bool // Direct dependencies (from go.mod require)
}
// ModuleInfo represents a Go module dependency.
type ModuleInfo struct {
Path string // Module path (e.g., github.com/gin-gonic/gin)
Version string // Module version (e.g., v1.9.0)
IsDirect bool // Is this a direct dependency
Deps []string // Dependencies of this module
}
// newAnalyzer creates a new dependency analyzer.
func newAnalyzer() *analyzer {
return &analyzer{
packages: make(map[string]*goPackage),
visited: make(map[string]bool),
edges: make(map[string]bool),
store: &PackageStore{},
packages: make(map[string]*goPackage),
visited: make(map[string]bool),
edges: make(map[string]bool),
store: &PackageStore{},
modules: make(map[string]*ModuleInfo),
moduleGraph: make(map[string][]string),
directModules: make(map[string]bool),
}
}
@ -220,7 +233,423 @@ func (a *analyzer) loadPackages(ctx context.Context, pkgPath string) error {
return nil
}
// loadModuleGraph loads module-level dependencies using go mod graph.
// This provides module-level dependency information (not package-level).
func (a *analyzer) loadModuleGraph(ctx context.Context) error {
// First, get direct dependencies from go list -m
directCmd := "go list -m -json all"
directResult, err := gproc.ShellExec(ctx, directCmd)
if err != nil {
return fmt.Errorf("failed to execute go list -m: %v", err)
}
// Parse direct dependencies
directDecoder := json.NewDecoder(strings.NewReader(directResult))
for directDecoder.More() {
var mod struct {
Path string `json:"Path"`
Version string `json:"Version"`
Main bool `json:"Main"`
Indirect bool `json:"Indirect"`
}
if err := directDecoder.Decode(&mod); err != nil {
continue
}
// Skip main module
if mod.Main {
continue
}
// Track direct vs indirect
if !mod.Indirect {
a.directModules[mod.Path] = true
}
// Create module info
a.modules[mod.Path] = &ModuleInfo{
Path: mod.Path,
Version: mod.Version,
IsDirect: !mod.Indirect,
Deps: []string{},
}
}
// Then, get module dependency graph
graphCmd := "go mod graph"
graphResult, err := gproc.ShellExec(ctx, graphCmd)
if err != nil {
return fmt.Errorf("failed to execute go mod graph: %v", err)
}
// Parse go mod graph output
// Format: module1@version module2@version
lines := gstr.Split(gstr.Trim(graphResult), "\n")
for _, line := range lines {
line = gstr.Trim(line)
if line == "" {
continue
}
parts := gstr.Split(line, " ")
if len(parts) != 2 {
continue
}
from := a.parseModulePath(parts[0])
to := a.parseModulePath(parts[1])
// Add to graph
a.moduleGraph[from] = append(a.moduleGraph[from], to)
// Ensure both modules exist in our map
if _, ok := a.modules[from]; !ok {
a.modules[from] = &ModuleInfo{
Path: from,
Version: a.parseModuleVersion(parts[0]),
IsDirect: a.directModules[from],
}
}
if _, ok := a.modules[to]; !ok {
a.modules[to] = &ModuleInfo{
Path: to,
Version: a.parseModuleVersion(parts[1]),
IsDirect: a.directModules[to],
}
}
// Update deps
a.modules[from].Deps = append(a.modules[from].Deps, to)
}
return nil
}
// parseModulePath extracts module path from "module@version" format.
func (a *analyzer) parseModulePath(s string) string {
if idx := gstr.Pos(s, "@"); idx > 0 {
return s[:idx]
}
return s
}
// parseModuleVersion extracts version from "module@version" format.
func (a *analyzer) parseModuleVersion(s string) string {
if idx := gstr.Pos(s, "@"); idx > 0 {
return s[idx+1:]
}
return ""
}
// generateModuleOutput generates output for module-level dependencies.
func (a *analyzer) generateModuleOutput(in Input) string {
switch in.Format {
case "tree":
return a.generateModuleTree(in)
case "list":
return a.generateModuleList(in)
case "mermaid":
return a.generateModuleMermaid(in)
case "dot":
return a.generateModuleDot(in)
case "json":
return a.generateModuleJSON(in)
default:
return a.generateModuleTree(in)
}
}
// generateModuleTree generates tree output for module dependencies.
func (a *analyzer) generateModuleTree(in Input) string {
var sb strings.Builder
// Get modules to display
modules := a.getFilteredModules(in)
sb.WriteString(fmt.Sprintf("%s (module dependencies)\n", a.modulePrefix))
// Show direct dependencies first
directDeps := make([]string, 0)
for _, mod := range modules {
if mod.IsDirect {
directDeps = append(directDeps, mod.Path)
}
}
sort.Strings(directDeps)
visited := make(map[string]bool)
for i, dep := range directDeps {
isLast := i == len(directDeps)-1
a.printModuleTreeNode(&sb, dep, "", isLast, in, visited, 0)
}
return sb.String()
}
// printModuleTreeNode prints a module tree node.
func (a *analyzer) printModuleTreeNode(sb *strings.Builder, modPath string, prefix string, isLast bool, in Input, visited map[string]bool, depth int) {
if in.Depth > 0 && depth >= in.Depth {
return
}
connector := "├── "
if isLast {
connector = "└── "
}
mod := a.modules[modPath]
version := ""
if mod != nil && mod.Version != "" {
version = "@" + mod.Version
}
sb.WriteString(prefix + connector + modPath + version + "\n")
// Skip if Direct mode and this is not a direct dependency
if in.Direct {
return
}
// Check for cycles
if visited[modPath] {
return
}
visited[modPath] = true
// Get dependencies
deps := a.moduleGraph[modPath]
sort.Strings(deps)
newPrefix := prefix
if isLast {
newPrefix += " "
} else {
newPrefix += "│ "
}
for i, dep := range deps {
depIsLast := i == len(deps)-1
a.printModuleTreeNode(sb, dep, newPrefix, depIsLast, in, visited, depth+1)
}
delete(visited, modPath)
}
// generateModuleList generates list output for module dependencies.
func (a *analyzer) generateModuleList(in Input) string {
var sb strings.Builder
modules := a.getFilteredModules(in)
// Count stats
directCount := 0
indirectCount := 0
for _, mod := range modules {
if mod.IsDirect {
directCount++
} else {
indirectCount++
}
}
sb.WriteString(fmt.Sprintf("# Module Dependencies for %s\n", a.modulePrefix))
sb.WriteString(fmt.Sprintf("# Direct: %d, Indirect: %d, Total: %d\n\n", directCount, indirectCount, len(modules)))
// Sort by path
paths := make([]string, 0, len(modules))
for path := range modules {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
mod := modules[path]
marker := ""
if mod.IsDirect {
marker = " (direct)"
}
sb.WriteString(fmt.Sprintf("%s@%s%s\n", mod.Path, mod.Version, marker))
}
return sb.String()
}
// generateModuleMermaid generates Mermaid diagram for module dependencies.
func (a *analyzer) generateModuleMermaid(in Input) string {
var sb strings.Builder
sb.WriteString("```mermaid\n")
sb.WriteString("graph TD\n")
edges := a.collectModuleEdges(in)
sortedEdges := make([]string, 0, len(edges))
for edge := range edges {
sortedEdges = append(sortedEdges, edge)
}
sort.Strings(sortedEdges)
for _, edge := range sortedEdges {
sb.WriteString(" " + edge + "\n")
}
sb.WriteString("```\n")
return sb.String()
}
// generateModuleDot generates Graphviz DOT for module dependencies.
func (a *analyzer) generateModuleDot(in Input) string {
var sb strings.Builder
sb.WriteString("digraph modules {\n")
sb.WriteString(" rankdir=TB;\n")
sb.WriteString(" node [shape=box];\n")
edges := a.collectModuleEdges(in)
sortedEdges := make([]string, 0, len(edges))
for edge := range edges {
sortedEdges = append(sortedEdges, edge)
}
sort.Strings(sortedEdges)
for _, edge := range sortedEdges {
parts := gstr.Split(edge, " --> ")
if len(parts) == 2 {
fmt.Fprintf(&sb, " \"%s\" -> \"%s\";\n", parts[0], parts[1])
}
}
sb.WriteString("}\n")
return sb.String()
}
// generateModuleJSON generates JSON output for module dependencies.
func (a *analyzer) generateModuleJSON(in Input) string {
modules := a.getFilteredModules(in)
result := make(map[string]any)
// Build module list
moduleList := make([]map[string]any, 0)
for _, mod := range modules {
m := map[string]any{
"path": mod.Path,
"version": mod.Version,
"direct": mod.IsDirect,
"depCount": len(mod.Deps),
}
if !in.Direct {
m["dependencies"] = mod.Deps
}
moduleList = append(moduleList, m)
}
// Sort by path
sort.Slice(moduleList, func(i, j int) bool {
return moduleList[i]["path"].(string) < moduleList[j]["path"].(string)
})
result["modules"] = moduleList
// Add statistics
directCount := 0
indirectCount := 0
for _, mod := range modules {
if mod.IsDirect {
directCount++
} else {
indirectCount++
}
}
result["statistics"] = map[string]any{
"total": len(modules),
"direct": directCount,
"indirect": indirectCount,
}
result["metadata"] = map[string]any{
"module": a.modulePrefix,
"format": in.Format,
"depth": in.Depth,
"direct": in.Direct,
}
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
return fmt.Sprintf("Error: %v", err)
}
return string(data)
}
// getFilteredModules returns modules based on filter options.
func (a *analyzer) getFilteredModules(in Input) map[string]*ModuleInfo {
result := make(map[string]*ModuleInfo)
for path, mod := range a.modules {
// Skip main module
if path == a.modulePrefix {
continue
}
// Filter by direct/indirect
if in.Direct && !mod.IsDirect {
continue
}
result[path] = mod
}
return result
}
// collectModuleEdges collects edges for module dependency graph.
func (a *analyzer) collectModuleEdges(in Input) map[string]bool {
edges := make(map[string]bool)
// Start from main module
if in.Direct {
// Only show direct dependencies from main module
for path, mod := range a.modules {
if mod.IsDirect {
from := a.sanitizeName(a.shortModuleName(a.modulePrefix))
to := a.sanitizeName(a.shortModuleName(path))
edge := fmt.Sprintf("%s --> %s", from, to)
edges[edge] = true
}
}
} else {
// Show full dependency graph
visited := make(map[string]bool)
a.collectModuleEdgesRecursive(a.modulePrefix, in, edges, visited, 0)
}
return edges
}
// collectModuleEdgesRecursive recursively collects module edges.
func (a *analyzer) collectModuleEdgesRecursive(modPath string, in Input, edges map[string]bool, visited map[string]bool, depth int) {
if in.Depth > 0 && depth >= in.Depth {
return
}
if visited[modPath] {
return
}
visited[modPath] = true
deps := a.moduleGraph[modPath]
fromName := a.sanitizeName(a.shortModuleName(modPath))
for _, dep := range deps {
toName := a.sanitizeName(a.shortModuleName(dep))
if fromName != toName {
edge := fmt.Sprintf("%s --> %s", fromName, toName)
edges[edge] = true
}
a.collectModuleEdgesRecursive(dep, in, edges, visited, depth+1)
}
}
// shortModuleName returns the module name (keeping full domain for clarity).
func (a *analyzer) shortModuleName(modPath string) string {
// Return full module path to preserve domain information
return modPath
}
// convertInputToFilterOptions converts legacy Input to new FilterOptions.
func (a *analyzer) convertInputToFilterOptions(in Input) *FilterOptions {
@ -228,7 +657,6 @@ func (a *analyzer) convertInputToFilterOptions(in Input) *FilterOptions {
IncludeInternal: in.Internal,
IncludeExternal: in.External,
IncludeStdLib: !in.NoStd,
MainModuleOnly: in.MainOnly,
Depth: in.Depth,
}
@ -321,11 +749,6 @@ func (opts *FilterOptions) Normalize(modulePrefix string) error {
// ShouldInclude determines if a package should be included based on filter options.
func (opts *FilterOptions) ShouldInclude(pkg *PackageInfo) bool {
// Filter by main module only (exclude submodules)
if opts.MainModuleOnly && !pkg.IsMainModule {
return false
}
// Filter by kind
switch pkg.Kind {
case KindStdLib:
@ -383,36 +806,6 @@ func (tc *TraversalContext) GetDependencies(pkg string) []string {
return result
}
// isMainModulePackage checks if a package belongs to the main module (not a submodule).
func (a *analyzer) isMainModulePackage(pkg string) bool {
if a.modulePrefix == "" {
return true // If no module prefix, consider all as main module
}
if !gstr.HasPrefix(pkg, a.modulePrefix) {
return false // Not even in our module
}
// Remove the module prefix to get the relative path
relativePath := gstr.TrimLeft(pkg[len(a.modulePrefix):], "/")
if relativePath == "" {
return true // This is the root module itself
}
// Check if this path contains a go.mod file (indicating a submodule)
// We check from the most specific path up to the root
parts := gstr.Split(relativePath, "/")
for i := len(parts); i > 0; i-- {
subPath := gstr.Join(parts[:i], "/")
if subPath != "" && gfile.Exists(subPath+"/go.mod") {
// Found a go.mod file in a subdirectory, this indicates a submodule
return false
}
}
return true // This is part of the main module
}
// shortName returns a shortened package name.
func (a *analyzer) shortName(pkg string, group bool) string {
if a.modulePrefix != "" && gstr.HasPrefix(pkg, a.modulePrefix) {
@ -534,7 +927,6 @@ func (a *analyzer) getDependencyStats(_ Input) map[string]any {
IncludeInternal: true,
IncludeExternal: true,
IncludeStdLib: true,
MainModuleOnly: false,
Depth: 0,
}
@ -639,11 +1031,10 @@ func (a *analyzer) buildPackageStore() *PackageStore {
// Convert go packages to PackageInfo
for path, goPkg := range a.packages {
pkgInfo := &PackageInfo{
ImportPath: path,
ModulePath: goPkg.Module.Path,
IsStdLib: goPkg.Standard,
Imports: goPkg.Imports,
IsMainModule: a.isMainModulePackage(path),
ImportPath: path,
ModulePath: goPkg.Module.Path,
IsStdLib: goPkg.Standard,
Imports: goPkg.Imports,
}
pkgInfo.Kind = store.identifyPackageKind(pkgInfo)
store.packages[path] = pkgInfo

View File

@ -16,22 +16,46 @@ func TestExternalDependencyAnalysis(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
analyzer := newAnalyzer()
analyzer.modulePrefix = "github.com/gogf/gf/cmd/gf/v2"
analyzer.packages = map[string]*goPackage{
"github.com/other/package": {
ImportPath: "github.com/other/package",
Standard: false,
},
"github.com/gogf/gf/cmd/gf/v2/internal": {
ImportPath: "github.com/gogf/gf/cmd/gf/v2/internal",
Standard: false,
},
"fmt": {
ImportPath: "fmt",
Standard: true,
},
}
// Test shouldInclude with external dependencies
// Test using new FilterOptions system
in := Input{
Internal: false,
External: true,
NoStd: true,
}
opts := analyzer.convertInputToFilterOptions(in)
opts.Normalize(analyzer.modulePrefix)
store := analyzer.buildPackageStore()
// Test external package (should be included)
t.Assert(analyzer.shouldInclude("github.com/other/package", in), true)
externalPkg, ok := store.packages["github.com/other/package"]
t.Assert(ok, true)
t.Assert(opts.ShouldInclude(externalPkg), true)
// Test internal package (should not be included)
t.Assert(analyzer.shouldInclude("github.com/gogf/gf/cmd/gf/v2/internal", in), false)
internalPkg, ok := store.packages["github.com/gogf/gf/cmd/gf/v2/internal"]
t.Assert(ok, true)
t.Assert(opts.ShouldInclude(internalPkg), false)
// Test standard library (should not be included due to NoStd)
t.Assert(analyzer.shouldInclude("fmt", in), false)
stdPkg, ok := store.packages["fmt"]
t.Assert(ok, true)
t.Assert(opts.ShouldInclude(stdPkg), false)
})
}
@ -39,11 +63,11 @@ func TestExternalGrouping(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
analyzer := newAnalyzer()
// Test external group extraction
t.Assert(analyzer.getExternalGroup("github.com/user/repo"), "github.com/user")
t.Assert(analyzer.getExternalGroup("golang.org/x/tools"), "golang.org")
t.Assert(analyzer.getExternalGroup("fmt"), "stdlib")
t.Assert(analyzer.getExternalGroup("simple"), "simple")
// Test external group extraction using shortName
t.Assert(analyzer.shortName("github.com/user/repo", false), "github.com/user/repo")
t.Assert(analyzer.shortName("golang.org/x/tools", false), "golang.org/x/tools")
t.Assert(analyzer.shortName("fmt", false), "fmt")
t.Assert(analyzer.shortName("simple", false), "simple")
})
}

View File

@ -306,7 +306,6 @@ func (a *analyzer) generateJSON(in Input) string {
"internal": in.Internal,
"external": in.External,
"nostd": in.NoStd,
"main": in.MainOnly,
}
data, err := json.MarshalIndent(result, "", " ")

View File

@ -7,6 +7,7 @@
package cmddep
import (
"context"
"embed"
"encoding/json"
"fmt"
@ -177,11 +178,30 @@ func (a *analyzer) handleGraphAPI(w http.ResponseWriter, r *http.Request, in Inp
if e := query.Get("external"); e != "" {
in.External = e == "true"
}
if m := query.Get("main"); m != "" {
in.MainOnly = m == "true"
if m := query.Get("module"); m != "" {
in.Module = m == "true"
}
if d := query.Get("direct"); d != "" {
in.Direct = d == "true"
}
pkg := query.Get("package")
// Module-level graph
if in.Module {
// Ensure module graph is loaded
if len(a.modules) == 0 {
if err := a.loadModuleGraph(context.Background()); err != nil {
http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError)
return
}
}
data := a.buildModuleGraphData(in)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
return
}
var data *graphData
if pkg != "" {
data = a.buildPackageGraphData(pkg, in)
@ -192,7 +212,7 @@ func (a *analyzer) handleGraphAPI(w http.ResponseWriter, r *http.Request, in Inp
json.NewEncoder(w).Encode(data)
}
// handlePackagesAPI returns all packages list with dependency stats.
// handlePackagesAPI returns all packages list with dependency stats using new system.
func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in Input) {
query := r.URL.Query()
if i := query.Get("internal"); i != "" {
@ -201,31 +221,48 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in
if e := query.Get("external"); e != "" {
in.External = e == "true"
}
if m := query.Get("main"); m != "" {
in.MainOnly = m == "true"
if m := query.Get("module"); m != "" {
in.Module = m == "true"
}
if d := query.Get("direct"); d != "" {
in.Direct = d == "true"
}
// Module-level packages
if in.Module {
a.handleModulesAPI(w, in)
return
}
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
// Build reverse dependency map (who uses each package)
usedByMap := make(map[string]int)
for fullPath, pkg := range a.packages {
if !a.shouldInclude(fullPath, in) {
for fullPath, pkgInfo := range store.packages {
if !opts.ShouldInclude(pkgInfo) {
continue
}
fromShort := a.shortName(fullPath, false)
if fromShort == "" {
continue
}
for _, dep := range a.filterDeps(pkg.Imports, in) {
shortDep := a.shortName(dep, false)
if shortDep != "" {
usedByMap[shortDep]++
for _, dep := range pkgInfo.Imports {
depInfo, ok := store.packages[dep]
if ok && opts.ShouldInclude(depInfo) {
shortDep := a.shortName(dep, false)
if shortDep != "" {
usedByMap[shortDep]++
}
}
}
}
packages := make([]packageSummary, 0)
for _, pkgPath := range a.getSortedPackages() {
if !a.shouldInclude(pkgPath, in) {
pkgInfo, ok := store.packages[pkgPath]
if !ok || !opts.ShouldInclude(pkgInfo) {
continue
}
shortName := a.shortName(pkgPath, false)
@ -235,8 +272,9 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in
// Count dependencies (filtered)
depCount := 0
if pkg, ok := a.packages[pkgPath]; ok {
for _, dep := range a.filterDeps(pkg.Imports, in) {
for _, dep := range pkgInfo.Imports {
depInfo, ok := store.packages[dep]
if ok && opts.ShouldInclude(depInfo) {
shortDep := a.shortName(dep, false)
if shortDep != "" {
depCount++
@ -261,7 +299,7 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in
json.NewEncoder(w).Encode(result)
}
// handlePackageAPI returns detailed info for a specific package.
// handlePackageAPI returns detailed info for a specific package using new system.
func (a *analyzer) handlePackageAPI(w http.ResponseWriter, r *http.Request, in Input) {
query := r.URL.Query()
pkgName := query.Get("name")
@ -284,7 +322,16 @@ func (a *analyzer) handlePackageAPI(w http.ResponseWriter, r *http.Request, in I
return
}
pkg := a.packages[fullPath]
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
pkgInfo, ok := store.packages[fullPath]
if !ok {
http.Error(w, "package not found", http.StatusNotFound)
return
}
info := packageInfo{
Name: pkgName,
FullPath: fullPath,
@ -293,18 +340,21 @@ func (a *analyzer) handlePackageAPI(w http.ResponseWriter, r *http.Request, in I
}
// Get dependencies
for _, dep := range a.filterDeps(pkg.Imports, in) {
shortName := a.shortName(dep, false)
if shortName != "" {
info.Dependencies = append(info.Dependencies, shortName)
for _, dep := range pkgInfo.Imports {
depInfo, ok := store.packages[dep]
if ok && opts.ShouldInclude(depInfo) {
shortName := a.shortName(dep, false)
if shortName != "" {
info.Dependencies = append(info.Dependencies, shortName)
}
}
}
sort.Strings(info.Dependencies)
// Get reverse dependencies (who uses this package)
for path, p := range a.packages {
for path, p := range store.packages {
for _, dep := range p.Imports {
if dep == fullPath {
if dep == fullPath && opts.ShouldInclude(p) {
shortName := a.shortName(path, false)
if shortName != "" {
info.UsedBy = append(info.UsedBy, shortName)
@ -331,11 +381,30 @@ func (a *analyzer) handleTreeAPI(w http.ResponseWriter, r *http.Request, in Inpu
if e := query.Get("external"); e != "" {
in.External = e == "true"
}
if m := query.Get("main"); m != "" {
in.MainOnly = m == "true"
if m := query.Get("module"); m != "" {
in.Module = m == "true"
}
if d := query.Get("direct"); d != "" {
in.Direct = d == "true"
}
pkg := query.Get("package")
// Module-level tree
if in.Module {
// Ensure module graph is loaded
if len(a.modules) == 0 {
if err := a.loadModuleGraph(context.Background()); err != nil {
http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError)
return
}
}
output := a.generateModuleTree(in)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte(output))
return
}
var output string
if pkg != "" {
output = a.generatePackageTree(pkg, in)
@ -356,11 +425,30 @@ func (a *analyzer) handleListAPI(w http.ResponseWriter, r *http.Request, in Inpu
if e := query.Get("external"); e != "" {
in.External = e == "true"
}
if m := query.Get("main"); m != "" {
in.MainOnly = m == "true"
if m := query.Get("module"); m != "" {
in.Module = m == "true"
}
if d := query.Get("direct"); d != "" {
in.Direct = d == "true"
}
pkg := query.Get("package")
// Module-level list
if in.Module {
// Ensure module graph is loaded
if len(a.modules) == 0 {
if err := a.loadModuleGraph(context.Background()); err != nil {
http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError)
return
}
}
output := a.generateModuleList(in)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte(output))
return
}
var output string
if pkg != "" {
output = a.generatePackageList(pkg, in)
@ -412,7 +500,7 @@ func (a *analyzer) buildGraphData(in Input) *graphData {
return data
}
// buildPackageGraphData builds graph data for a specific package.
// buildPackageGraphData builds graph data for a specific package using new system.
func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData {
data := &graphData{
Nodes: make([]graphNode, 0),
@ -432,6 +520,15 @@ func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData {
return data
}
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
_, ok := store.packages[fullPath]
if !ok {
return data
}
nodeSet := make(map[string]bool)
nodeSet[pkgName] = true
data.Nodes = append(data.Nodes, graphNode{
@ -444,9 +541,9 @@ func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData {
if in.Reverse {
// Show packages that depend on this package
for path, p := range a.packages {
for path, p := range store.packages {
for _, dep := range p.Imports {
if dep == fullPath {
if dep == fullPath && opts.ShouldInclude(p) {
shortName := a.shortName(path, false)
if shortName != "" && !nodeSet[shortName] {
nodeSet[shortName] = true
@ -472,14 +569,27 @@ func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData {
return data
}
// collectPackageDeps recursively collects dependencies for a package.
// collectPackageDeps recursively collects dependencies for a package using new system.
func (a *analyzer) collectPackageDeps(pkg *goPackage, pkgName string, in Input, nodeSet map[string]bool, data *graphData, depth int) {
if in.Depth > 0 && depth >= in.Depth {
return
}
deps := a.filterDeps(pkg.Imports, in)
for _, dep := range deps {
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
pkgInfo, ok := store.packages[pkg.ImportPath]
if !ok {
return
}
for _, dep := range pkgInfo.Imports {
depInfo, ok := store.packages[dep]
if !ok || !opts.ShouldInclude(depInfo) {
continue
}
shortName := a.shortName(dep, false)
if shortName == "" {
continue
@ -506,7 +616,7 @@ func (a *analyzer) collectPackageDeps(pkg *goPackage, pkgName string, in Input,
}
}
// generatePackageTree generates tree output for a specific package.
// generatePackageTree generates tree output for a specific package using new system.
func (a *analyzer) generatePackageTree(pkgName string, in Input) string {
var fullPath string
for path := range a.packages {
@ -520,15 +630,29 @@ func (a *analyzer) generatePackageTree(pkgName string, in Input) string {
return "Package not found: " + pkgName
}
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
_, ok := store.packages[fullPath]
if !ok {
return "Package not found: " + pkgName
}
var sb strings.Builder
pkg := a.packages[fullPath]
a.visited = make(map[string]bool)
ctx := &TraversalContext{
visited: make(map[string]bool),
options: opts,
store: store,
maxDepth: opts.Depth,
}
sb.WriteString(pkgName + "\n")
a.printTreeNode(&sb, pkg, "", in, 0)
a.printTreeNodeNew(&sb, fullPath, "", in, ctx, 0)
return sb.String()
}
// generatePackageList generates list output for a specific package.
// generatePackageList generates list output for a specific package using new system.
func (a *analyzer) generatePackageList(pkgName string, in Input) string {
var fullPath string
for path := range a.packages {
@ -542,15 +666,25 @@ func (a *analyzer) generatePackageList(pkgName string, in Input) string {
return "Package not found: " + pkgName
}
var sb strings.Builder
pkg := a.packages[fullPath]
deps := a.filterDeps(pkg.Imports, in)
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
shortDeps := make([]string, 0, len(deps))
for _, dep := range deps {
shortName := a.shortName(dep, false)
if shortName != "" {
shortDeps = append(shortDeps, shortName)
pkgInfo, ok := store.packages[fullPath]
if !ok {
return "Package not found: " + pkgName
}
var sb strings.Builder
shortDeps := make([]string, 0)
for _, dep := range pkgInfo.Imports {
depInfo, ok := store.packages[dep]
if ok && opts.ShouldInclude(depInfo) {
shortName := a.shortName(dep, false)
if shortName != "" {
shortDeps = append(shortDeps, shortName)
}
}
}
sort.Strings(shortDeps)
@ -743,6 +877,94 @@ func (s *serverState) handleAnalyzeAPI(w http.ResponseWriter, r *http.Request) {
return
}
// IMPORTANT: Load module dependencies from the actual module directory
// We need to run commands in moduleDir context to get the correct direct/indirect info
// Running in tempDir would show everything as indirect since we're not using them there
// IMPORTANT: Load direct dependencies info FIRST
// This must be done before parsing go mod graph so that IsDirect flag is correct
listModCmd := exec.Command("go", "list", "-m", "-json", "all")
listModCmd.Dir = moduleDir // Run in the actual module directory
modOutput, err := listModCmd.CombinedOutput()
if err == nil {
modDecoder := json.NewDecoder(strings.NewReader(string(modOutput)))
for modDecoder.More() {
var mod struct {
Path string `json:"Path"`
Version string `json:"Version"`
Main bool `json:"Main"`
Indirect bool `json:"Indirect"`
}
if err := modDecoder.Decode(&mod); err != nil {
continue
}
// Skip main module
if mod.Main {
continue
}
// Track direct vs indirect
if !mod.Indirect {
newAnalyzer.directModules[mod.Path] = true
}
// Create module info
newAnalyzer.modules[mod.Path] = &ModuleInfo{
Path: mod.Path,
Version: mod.Version,
IsDirect: !mod.Indirect,
Deps: []string{},
}
}
}
// Load module-level dependencies for the remote module
// Run go mod graph in the module directory context
graphCmd := exec.Command("go", "mod", "graph")
graphCmd.Dir = moduleDir // Run in the actual module directory
graphOutput, err := graphCmd.CombinedOutput()
if err == nil {
// Parse module graph
lines := strings.Split(strings.TrimSpace(string(graphOutput)), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) != 2 {
continue
}
from := newAnalyzer.parseModulePath(parts[0])
to := newAnalyzer.parseModulePath(parts[1])
// Add to graph
newAnalyzer.moduleGraph[from] = append(newAnalyzer.moduleGraph[from], to)
// Ensure both modules exist in our map (use existing or create with correct IsDirect)
if _, ok := newAnalyzer.modules[from]; !ok {
newAnalyzer.modules[from] = &ModuleInfo{
Path: from,
Version: newAnalyzer.parseModuleVersion(parts[0]),
IsDirect: newAnalyzer.directModules[from],
}
}
if _, ok := newAnalyzer.modules[to]; !ok {
newAnalyzer.modules[to] = &ModuleInfo{
Path: to,
Version: newAnalyzer.parseModuleVersion(parts[1]),
IsDirect: newAnalyzer.directModules[to],
}
}
// Update deps
newAnalyzer.modules[from].Deps = append(newAnalyzer.modules[from].Deps, to)
}
}
s.currentAnalyzer = newAnalyzer
w.Header().Set("Content-Type", "application/json")
@ -819,3 +1041,105 @@ func (s *serverState) handleResetAPI(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// handleModulesAPI returns module-level dependency list.
func (a *analyzer) handleModulesAPI(w http.ResponseWriter, in Input) {
// Ensure module graph is loaded
if len(a.modules) == 0 {
if err := a.loadModuleGraph(context.Background()); err != nil {
http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError)
return
}
}
modules := a.getFilteredModules(in)
// Build module list
moduleList := make([]map[string]any, 0)
for _, mod := range modules {
m := map[string]any{
"name": mod.Path,
"depCount": len(mod.Deps),
"usedByCount": 0, // TODO: calculate reverse deps
"version": mod.Version,
"direct": mod.IsDirect,
}
moduleList = append(moduleList, m)
}
// Sort by path
sort.Slice(moduleList, func(i, j int) bool {
return moduleList[i]["name"].(string) < moduleList[j]["name"].(string)
})
// Count stats
directCount := 0
indirectCount := 0
for _, mod := range modules {
if mod.IsDirect {
directCount++
} else {
indirectCount++
}
}
result := map[string]any{
"packages": moduleList,
"statistics": map[string]any{
"total": len(modules),
"direct": directCount,
"indirect": indirectCount,
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// buildModuleGraphData builds graph data for module-level visualization.
func (a *analyzer) buildModuleGraphData(in Input) *graphData {
data := &graphData{
Nodes: make([]graphNode, 0),
Edges: make([]graphEdge, 0),
}
nodeSet := make(map[string]bool)
edges := a.collectModuleEdges(in)
for edge := range edges {
parts := strings.Split(edge, " --> ")
if len(parts) != 2 {
continue
}
from, to := parts[0], parts[1]
if !nodeSet[from] {
nodeSet[from] = true
group := "indirect"
if mod, ok := a.modules[from]; ok && mod.IsDirect {
group = "direct"
}
data.Nodes = append(data.Nodes, graphNode{
ID: from,
Label: strings.ReplaceAll(from, "_", "/"),
Group: group,
})
}
if !nodeSet[to] {
nodeSet[to] = true
group := "indirect"
if mod, ok := a.modules[to]; ok && mod.IsDirect {
group = "direct"
}
data.Nodes = append(data.Nodes, graphNode{
ID: to,
Label: strings.ReplaceAll(to, "_", "/"),
Group: group,
})
}
data.Edges = append(data.Edges, graphEdge{From: from, To: to})
}
return data
}

View File

@ -13,6 +13,129 @@ import (
"github.com/gogf/gf/v2/test/gtest"
)
// Test data model creation and classification
func Test_PackageInfo_Creation(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
pkg := &PackageInfo{
ImportPath: "github.com/gogf/gf/v2/os/gfile",
ModulePath: "github.com/gogf/gf/v2",
Kind: KindInternal,
Tier: 2,
Imports: []string{"fmt", "os"},
IsStdLib: false,
IsModuleRoot: false,
}
t.Assert(pkg != nil, true)
t.Assert(pkg.Kind, KindInternal)
t.Assert(pkg.Tier, 2)
t.Assert(len(pkg.Imports), 2)
})
}
// Test FilterOptions normalization
func Test_FilterOptions_Normalize(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
opts := &FilterOptions{
IncludeInternal: false,
IncludeExternal: false,
IncludeStdLib: false,
}
opts.Normalize("github.com/gogf/gf/v2")
// After normalization, internal should be included by default
t.Assert(opts.IncludeInternal, true)
t.Assert(opts.IncludeExternal, false)
})
}
// Test ShouldInclude decision logic
func Test_FilterOptions_ShouldInclude(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test internal package inclusion
opts := &FilterOptions{
IncludeInternal: true,
IncludeExternal: false,
IncludeStdLib: true,
}
internalPkg := &PackageInfo{
Kind: KindInternal,
IsStdLib: false,
}
externalPkg := &PackageInfo{
Kind: KindExternal,
IsStdLib: false,
}
stdlibPkg := &PackageInfo{
Kind: KindStdLib,
IsStdLib: true,
}
t.Assert(opts.ShouldInclude(internalPkg), true)
t.Assert(opts.ShouldInclude(externalPkg), false)
t.Assert(opts.ShouldInclude(stdlibPkg), true)
})
}
// Test TraversalContext Visit tracking
func Test_TraversalContext_Visit(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := &TraversalContext{
visited: make(map[string]bool),
}
// First visit should return false
t.Assert(ctx.Visit("pkg1"), false)
// Second visit should return true
t.Assert(ctx.Visit("pkg1"), true)
// New package should return false
t.Assert(ctx.Visit("pkg2"), false)
})
}
// Test guessModuleRoot function
func Test_GuessModuleRoot(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
tests := []struct {
input string
expect string
}{
{"github.com/gogf/gf", "github.com/gogf/gf"},
{"github.com/gogf/gf/v2", "github.com/gogf/gf/v2"},
{"github.com/gogf/gf/v2/os/gfile", "github.com/gogf/gf/v2"},
{"github.com/gogf/gf/v2/os", "github.com/gogf/gf/v2"},
}
for _, test := range tests {
result := guessModuleRoot(test.input)
t.AssertEQ(result, test.expect)
}
})
}
// Test input to filter options conversion
func Test_ConvertInputToFilterOptions(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
a := newAnalyzer()
input := Input{
Internal: true,
External: false,
NoStd: true,
Module: false,
Direct: false,
Depth: 3,
}
opts := a.convertInputToFilterOptions(input)
t.Assert(opts.IncludeInternal, true)
t.Assert(opts.IncludeExternal, false)
t.Assert(opts.IncludeStdLib, false) // NoStd=true means !IncludeStdLib
t.Assert(opts.Depth, 3)
})
}
func Test_Dep_Tree(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
@ -112,3 +235,21 @@ func Test_Dep_Group(t *testing.T) {
t.AssertNil(err)
})
}
func Test_ModuleLevel_Direct(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
// Test module level with direct only
in := Input{
Module: true,
Direct: true,
Format: "list",
}
t.Logf("Input.Module: %v, Input.Direct: %v", in.Module, in.Direct)
_, err := Dep.Index(ctx, in)
t.AssertNil(err)
})
}

View File

@ -123,7 +123,11 @@ async function fetchVersions() {
const versionSelect = document.getElementById('versionSelect');
const spinner = document.getElementById('loadingSpinner');
const modulePath = input.value.trim();
let modulePath = input.value.trim();
// Remove http:// or https:// prefix if present
modulePath = modulePath.replace(/^https?:\/\//, '');
input.value = modulePath;
if (!modulePath) {
versionSelect.disabled = true;
versionSelect.innerHTML = `<option value="">${i18n.t('selectVersion')}</option>`;
@ -163,7 +167,11 @@ async function analyzeRemoteModule() {
const spinner = document.getElementById('loadingSpinner');
const analyzeBtn = document.getElementById('analyzeBtn');
const modulePath = input.value.trim();
let modulePath = input.value.trim();
// Remove http:// or https:// prefix if present
modulePath = modulePath.replace(/^https?:\/\//, '');
input.value = modulePath;
const version = versionSelect.value;
if (!modulePath) {
@ -410,8 +418,9 @@ async function loadPackages() {
try {
const internal = document.getElementById('internal').checked;
const external = document.getElementById('external') ? document.getElementById('external').checked : false;
const mainOnly = document.getElementById('mainOnly') ? document.getElementById('mainOnly').checked : false;
const response = await fetch(`/api/packages?internal=${internal}&external=${external}&main=${mainOnly}`);
const moduleLevel = document.getElementById('moduleLevel') ? document.getElementById('moduleLevel').checked : false;
const directOnly = document.getElementById('directOnly') ? document.getElementById('directOnly').checked : false;
const response = await fetch(`/api/packages?internal=${internal}&external=${external}&module=${moduleLevel}&direct=${directOnly}`);
const data = await response.json();
// Handle new API response format with packages and statistics
@ -678,7 +687,8 @@ async function refresh() {
const reverse = document.getElementById('reverse').checked;
const internal = document.getElementById('internal').checked;
const external = document.getElementById('external') ? document.getElementById('external').checked : false;
const mainOnly = document.getElementById('mainOnly') ? document.getElementById('mainOnly').checked : false;
const moduleLevel = document.getElementById('moduleLevel') ? document.getElementById('moduleLevel').checked : false;
const directOnly = document.getElementById('directOnly') ? document.getElementById('directOnly').checked : false;
if (selectedPackage) {
await showPackageInfo(selectedPackage);
@ -687,11 +697,11 @@ async function refresh() {
}
if (currentView === 'graph') {
await refreshGraph(depth, group, reverse, internal, external, mainOnly);
await refreshGraph(depth, group, reverse, internal, external, moduleLevel, directOnly);
} else if (currentView === 'tree') {
await refreshTree(depth, internal, external, mainOnly);
await refreshTree(depth, internal, external, moduleLevel, directOnly);
} else {
await refreshList(internal, external, mainOnly);
await refreshList(internal, external, moduleLevel, directOnly);
}
}
@ -724,7 +734,7 @@ async function showPackageInfo(pkg) {
}
// Refresh graph view
async function refreshGraph(depth, group, reverse, internal, external, mainOnly) {
async function refreshGraph(depth, group, reverse, internal, external, moduleLevel, directOnly) {
document.getElementById('graphView').classList.remove('hidden');
document.getElementById('textView').classList.add('hidden');
@ -738,8 +748,11 @@ async function refreshGraph(depth, group, reverse, internal, external, mainOnly)
if (external !== undefined) {
url += `&external=${external}`;
}
if (mainOnly !== undefined) {
url += `&main=${mainOnly}`;
if (moduleLevel !== undefined) {
url += `&module=${moduleLevel}`;
}
if (directOnly !== undefined) {
url += `&direct=${directOnly}`;
}
if (selectedPackage) {
url += '&package=' + encodeURIComponent(selectedPackage);
@ -815,7 +828,7 @@ function autoFitGraph() {
}
// Refresh tree view
async function refreshTree(depth, internal, external, mainOnly) {
async function refreshTree(depth, internal, external, moduleLevel, directOnly) {
document.getElementById('graphView').classList.add('hidden');
document.getElementById('textView').classList.remove('hidden');
@ -823,8 +836,11 @@ async function refreshTree(depth, internal, external, mainOnly) {
if (external !== undefined) {
url += `&external=${external}`;
}
if (mainOnly !== undefined) {
url += `&main=${mainOnly}`;
if (moduleLevel !== undefined) {
url += `&module=${moduleLevel}`;
}
if (directOnly !== undefined) {
url += `&direct=${directOnly}`;
}
if (selectedPackage) {
url += '&package=' + encodeURIComponent(selectedPackage);
@ -844,7 +860,7 @@ async function refreshTree(depth, internal, external, mainOnly) {
}
// Refresh list view
async function refreshList(internal, external, mainOnly) {
async function refreshList(internal, external, moduleLevel, directOnly) {
document.getElementById('graphView').classList.add('hidden');
document.getElementById('textView').classList.remove('hidden');
@ -852,8 +868,11 @@ async function refreshList(internal, external, mainOnly) {
if (external !== undefined) {
url += `&external=${external}`;
}
if (mainOnly !== undefined) {
url += `&main=${mainOnly}`;
if (moduleLevel !== undefined) {
url += `&module=${moduleLevel}`;
}
if (directOnly !== undefined) {
url += `&direct=${directOnly}`;
}
if (selectedPackage) {
url += '&package=' + encodeURIComponent(selectedPackage);

View File

@ -24,7 +24,8 @@ const i18n = {
groupLabel: 'Group by directory',
internalLabel: 'Internal only',
externalLabel: 'External packages',
mainOnlyLabel: 'Main module only',
moduleLevelLabel: 'Module level',
directOnlyLabel: 'Direct only',
statsInternal: 'Internal',
statsExternal: 'External',
statsStdlib: 'Stdlib',
@ -61,7 +62,8 @@ const i18n = {
groupTooltip: 'Group packages by directory structure',
internalTooltip: 'Show only internal packages from current module',
externalTooltip: 'Include external dependency packages',
mainOnlyTooltip: 'Show only main module packages (exclude vendor)',
moduleLevelTooltip: 'Show module-level dependencies (from go.mod)',
directOnlyTooltip: 'Show only direct dependencies (requires module level)',
layoutTDTooltip: 'Top-down layout for dependency graph',
layoutLRTooltip: 'Left-right layout for dependency graph',
showAllTooltip: 'Clear package selection and show all packages',
@ -100,7 +102,8 @@ const i18n = {
groupLabel: '按目录分组',
internalLabel: '仅内部包',
externalLabel: '外部依赖包',
mainOnlyLabel: '仅主模块',
moduleLevelLabel: '模块级别',
directOnlyLabel: '仅直接依赖',
statsInternal: '内部',
statsExternal: '外部',
statsStdlib: '标准库',
@ -137,7 +140,8 @@ const i18n = {
groupTooltip: '按目录结构分组显示包',
internalTooltip: '仅显示当前模块的内部包',
externalTooltip: '包含外部依赖包',
mainOnlyTooltip: '显示模块排除vendor目录',
moduleLevelTooltip: '显示模块级别依赖来自go.mod',
directOnlyTooltip: '仅显示直接依赖需要启用模块级别',
layoutTDTooltip: '依赖图从上到下布局',
layoutLRTooltip: '依赖图从左到右布局',
showAllTooltip: '清除包选择显示所有包',

View File

@ -79,8 +79,12 @@
<label for="external" data-i18n="externalLabel">External packages</label>
</div>
<div class="control-group">
<input type="checkbox" id="mainOnly" onchange="refresh()" data-i18n-title="mainOnlyTooltip">
<label for="mainOnly" data-i18n="mainOnlyLabel">Main module only</label>
<input type="checkbox" id="moduleLevel" onchange="refresh()" data-i18n-title="moduleLevelTooltip">
<label for="moduleLevel" data-i18n="moduleLevelLabel">Module level</label>
</div>
<div class="control-group">
<input type="checkbox" id="directOnly" onchange="refresh()" data-i18n-title="directOnlyTooltip">
<label for="directOnly" data-i18n="directOnlyLabel">Direct only</label>
</div>
<div class="control-group graph-only" id="layoutGroup">
<label data-i18n="layoutLabel">Layout:</label>