diff --git a/cmd/gf/internal/cmd/cmddep/cmddep.go b/cmd/gf/internal/cmd/cmddep/cmddep.go index 7d8115da8..6d7739b3e 100644 --- a/cmd/gf/internal/cmd/cmddep/cmddep.go +++ b/cmd/gf/internal/cmd/cmddep/cmddep.go @@ -37,9 +37,15 @@ gf dep -f json -d 0 gf dep -g gf dep -r gf dep -i=false +gf dep -e +gf dep -e -i=false +gf dep -m +gf dep -m -e 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 ` ) @@ -58,6 +64,8 @@ type Input struct { 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"` Serve bool `name:"serve" short:"s" brief:"start HTTP server to view dependencies" d:"false" orphan:"true"` diff --git a/cmd/gf/internal/cmd/cmddep/cmddep_analyzer.go b/cmd/gf/internal/cmd/cmddep/cmddep_analyzer.go index b114f2749..9fc675c9b 100644 --- a/cmd/gf/internal/cmd/cmddep/cmddep_analyzer.go +++ b/cmd/gf/internal/cmd/cmddep/cmddep_analyzer.go @@ -24,6 +24,9 @@ type goPackage struct { Imports []string `json:"Imports"` Deps []string `json:"Deps"` Standard bool `json:"Standard"` + Module struct { + Path string `json:"Path"` + } `json:"Module"` } // depNode represents a node in the dependency tree. @@ -67,10 +70,14 @@ func (a *analyzer) detectModulePrefix() string { // loadPackages loads package information using go list. func (a *analyzer) loadPackages(ctx context.Context, pkgPath string) error { + // Load main packages first cmd := fmt.Sprintf("go list -json %s", pkgPath) result, err := gproc.ShellExec(ctx, cmd) if err != nil { - return fmt.Errorf("failed to execute go list: %v", err) + // Try to get more detailed error information + detailCmd := fmt.Sprintf("go list %s 2>&1", pkgPath) + detailResult, _ := gproc.ShellExec(ctx, detailCmd) + return fmt.Errorf("failed to execute go list: %v, details: %s", err, detailResult) } // Parse JSON stream (multiple JSON objects) @@ -82,14 +89,40 @@ func (a *analyzer) loadPackages(ctx context.Context, pkgPath string) error { } a.packages[pkg.ImportPath] = &pkg } + + // For external dependency analysis, also load dependencies + // This is optional and won't fail the entire operation + cmd = fmt.Sprintf("go list -json -deps %s", pkgPath) + result, err = gproc.ShellExec(ctx, cmd) + if err == nil { + // Parse dependency JSON stream + decoder = json.NewDecoder(strings.NewReader(result)) + for decoder.More() { + var pkg goPackage + if err := decoder.Decode(&pkg); err != nil { + continue + } + // Only add if not already present + if _, exists := a.packages[pkg.ImportPath]; !exists { + a.packages[pkg.ImportPath] = &pkg + } + } + } return nil } // filterDeps filters dependencies based on options. func (a *analyzer) filterDeps(deps []string, in Input) []string { result := make([]string, 0) - for _, dep := range deps { - if a.shouldInclude(dep, in) { + seen := make(map[string]bool) + for _, original := range deps { + dep := original + if in.MainOnly { + dep = a.getModuleRoot(original) + } + + if a.shouldInclude(dep, in) && !seen[dep] { + seen[dep] = true result = append(result, dep) } } @@ -98,17 +131,34 @@ func (a *analyzer) filterDeps(deps []string, in Input) []string { // shouldInclude checks if a dependency should be included. func (a *analyzer) shouldInclude(dep string, in Input) bool { - // Exclude standard library + // Exclude standard library if requested if in.NoStd && a.isStdLib(dep) { return false } - // Only internal packages - if in.Internal && a.modulePrefix != "" { - if !gstr.HasPrefix(dep, a.modulePrefix) { + + isInternal := a.modulePrefix != "" && gstr.HasPrefix(dep, a.modulePrefix) + + // Handle main-only filtering - only keep module root packages + if in.MainOnly { + if dep != a.getModuleRoot(dep) { return false } } - return true + + // Handle internal/external filtering + if in.Internal && in.External { + // Show both internal and external + return true + } else if in.Internal && !in.External { + // Show only internal packages + return isInternal + } else if !in.Internal && in.External { + // Show only external packages + return !isInternal + } else { + // Default behavior: show internal packages only + return isInternal + } } // isStdLib checks if a package is from standard library. @@ -124,6 +174,82 @@ func (a *analyzer) isStdLib(pkg string) bool { return true } +// isModuleRootPackage checks if a package path is the root package of its module. +// It prefers go list Module.Path metadata; if missing, it falls back to guessing by domain/repo segments. +func (a *analyzer) isModuleRootPackage(pkg string) bool { + p, ok := a.packages[pkg] + if ok { + // Standard library has no module, treat as root + if p.Module.Path == "" { + return true + } + return p.Module.Path == p.ImportPath + } + + // Fallback: derive a plausible module root from the import path + return pkg == guessModuleRoot(pkg) +} + +// getModuleRoot returns the module root path for a package, using Module metadata when available. +func (a *analyzer) getModuleRoot(pkg string) string { + if p, ok := a.packages[pkg]; ok { + if p.Module.Path != "" { + return p.Module.Path + } + } + return guessModuleRoot(pkg) +} + +// guessModuleRoot tries to infer the module root path from an import path when Module metadata is missing. +// It keeps domain/owner/repo and also preserves a trailing /vN version segment if present. +func guessModuleRoot(pkg string) string { + parts := strings.Split(pkg, "/") + if len(parts) < 3 { + return pkg + } + + rootLen := 3 // domain/owner/repo + // Handle semantic import path version like .../v2 + if len(parts) > 3 && strings.HasPrefix(parts[3], "v") { + rootLen = 4 + } + + if rootLen > len(parts) { + rootLen = len(parts) + } + return strings.Join(parts[:rootLen], "/") +} + +// 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) { @@ -137,6 +263,35 @@ func (a *analyzer) shortName(pkg string, group bool) string { } return short } + + // For external packages, handle grouping differently + if group { + return a.getExternalGroup(pkg) + } + return pkg +} + +// getExternalGroup returns the group name for external packages. +func (a *analyzer) getExternalGroup(pkg string) string { + // For standard library packages + if a.isStdLib(pkg) { + return "stdlib" + } + + // For external packages, group by domain/organization + parts := gstr.Split(pkg, "/") + if len(parts) > 0 { + // Handle common patterns like github.com/user/repo + if len(parts) >= 3 && (parts[0] == "github.com" || parts[0] == "gitlab.com" || parts[0] == "bitbucket.org") { + return parts[0] + "/" + parts[1] + } + // For other domains, use the domain name + if gstr.Contains(parts[0], ".") { + return parts[0] + } + // For simple names, use the first part + return parts[0] + } return pkg } @@ -189,3 +344,39 @@ func (a *analyzer) collectEdgesRecursive(pkg *goPackage, in Input, edges map[str } } } + +// getDependencyStats returns statistics about dependencies. +func (a *analyzer) getDependencyStats(_ Input) map[string]any { + stats := make(map[string]any) + + var internalCount, externalCount, stdlibCount int + externalGroups := make(map[string]int) + + for _, pkg := range a.packages { + if !a.shouldInclude(pkg.ImportPath, Input{ + Internal: true, + External: true, + NoStd: false, + }) { + continue + } + + if a.isStdLib(pkg.ImportPath) { + stdlibCount++ + } else if a.modulePrefix != "" && gstr.HasPrefix(pkg.ImportPath, a.modulePrefix) { + internalCount++ + } else { + externalCount++ + group := a.getExternalGroup(pkg.ImportPath) + externalGroups[group]++ + } + } + + stats["total"] = len(a.packages) + stats["internal"] = internalCount + stats["external"] = externalCount + stats["stdlib"] = stdlibCount + stats["external_groups"] = externalGroups + + return stats +} diff --git a/cmd/gf/internal/cmd/cmddep/cmddep_external_test.go b/cmd/gf/internal/cmd/cmddep/cmddep_external_test.go new file mode 100644 index 000000000..d90680b18 --- /dev/null +++ b/cmd/gf/internal/cmd/cmddep/cmddep_external_test.go @@ -0,0 +1,83 @@ +// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package cmddep + +import ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" +) + +func TestExternalDependencyAnalysis(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + analyzer := newAnalyzer() + analyzer.modulePrefix = "github.com/gogf/gf/cmd/gf/v2" + + // Test shouldInclude with external dependencies + in := Input{ + Internal: false, + External: true, + NoStd: true, + } + + // Test external package (should be included) + t.Assert(analyzer.shouldInclude("github.com/other/package", in), true) + + // Test internal package (should not be included) + t.Assert(analyzer.shouldInclude("github.com/gogf/gf/cmd/gf/v2/internal", in), false) + + // Test standard library (should not be included due to NoStd) + t.Assert(analyzer.shouldInclude("fmt", in), false) + }) +} + +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") + }) +} + +func TestDependencyStats(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + analyzer := newAnalyzer() + analyzer.modulePrefix = "github.com/gogf/gf/cmd/gf/v2" + + // Add test packages + analyzer.packages = map[string]*goPackage{ + "github.com/gogf/gf/cmd/gf/v2/internal": { + ImportPath: "github.com/gogf/gf/cmd/gf/v2/internal", + Standard: false, + }, + "github.com/external/package": { + ImportPath: "github.com/external/package", + Standard: false, + }, + "fmt": { + ImportPath: "fmt", + Standard: true, + }, + } + + in := Input{ + Internal: true, + External: true, + NoStd: false, + } + + stats := analyzer.getDependencyStats(in) + t.Assert(stats["total"], 3) + t.Assert(stats["internal"], 1) + t.Assert(stats["external"], 1) + t.Assert(stats["stdlib"], 1) + }) +} \ No newline at end of file diff --git a/cmd/gf/internal/cmd/cmddep/cmddep_output.go b/cmd/gf/internal/cmd/cmddep/cmddep_output.go index d58b40bf2..ff7d77e93 100644 --- a/cmd/gf/internal/cmd/cmddep/cmddep_output.go +++ b/cmd/gf/internal/cmd/cmddep/cmddep_output.go @@ -27,6 +27,7 @@ func (a *analyzer) generate(in Input) string { case "json": return a.generateJSON(in) default: + // Default to tree format return a.generateTree(in) } } @@ -35,6 +36,24 @@ func (a *analyzer) generate(in Input) string { func (a *analyzer) generateTree(in Input) string { var sb strings.Builder + // Add statistics header if showing external dependencies + if in.External { + stats := a.getDependencyStats(in) + sb.WriteString("Dependency Statistics:\n") + fmt.Fprintf(&sb, " Total packages: %v\n", stats["total"]) + fmt.Fprintf(&sb, " Internal: %v\n", stats["internal"]) + fmt.Fprintf(&sb, " External: %v\n", stats["external"]) + fmt.Fprintf(&sb, " Standard library: %v\n", stats["stdlib"]) + + if groups, ok := stats["external_groups"].(map[string]int); ok && len(groups) > 0 { + sb.WriteString(" External groups:\n") + for group, count := range groups { + fmt.Fprintf(&sb, " %s: %d\n", group, count) + } + } + sb.WriteString("\nDependency Tree:\n") + } + // Find root packages (packages that are not imported by any other package) rootPkgs := a.findRootPackages() @@ -43,9 +62,11 @@ func (a *analyzer) generateTree(in Input) string { for _, pkgPath := range rootPkgs { pkg := a.packages[pkgPath] - shortName := a.shortName(pkg.ImportPath, in.Group) - sb.WriteString(shortName + "\n") - a.printTreeNode(&sb, pkg, "", in, 0) + if a.shouldInclude(pkg.ImportPath, in) { + shortName := a.shortName(pkg.ImportPath, in.Group) + sb.WriteString(shortName + "\n") + a.printTreeNode(&sb, pkg, "", in, 0) + } } return sb.String() } @@ -82,6 +103,7 @@ func (a *analyzer) printTreeNode(sb *strings.Builder, pkg *goPackage, prefix str return } + // filterDeps already applies all filtering including main-only deps := a.filterDeps(pkg.Imports, in) sort.Strings(deps) @@ -117,14 +139,37 @@ func (a *analyzer) printTreeNode(sb *strings.Builder, pkg *goPackage, prefix str // generateList generates simple list output. func (a *analyzer) generateList(in Input) string { var sb strings.Builder + + // Add statistics header if showing external dependencies + if in.External { + stats := a.getDependencyStats(in) + sb.WriteString("# Dependency Statistics\n") + fmt.Fprintf(&sb, "# Total: %v, Internal: %v, External: %v, Stdlib: %v\n", + stats["total"], stats["internal"], stats["external"], stats["stdlib"]) + sb.WriteString("\n") + } + + // Debug mainOnly state + // sb.WriteString(fmt.Sprintf("# DEBUG mainOnly=%v\\n", in.MainOnly)) + allDeps := make(map[string]bool) + // Collect dependencies from packages that should be included for _, pkg := range a.packages { - for _, dep := range a.filterDeps(pkg.Imports, in) { - allDeps[dep] = true + if a.shouldInclude(pkg.ImportPath, in) { + // Collect dependencies (filterDeps already applies all filtering including main-only) + for _, dep := range a.filterDeps(pkg.Imports, in) { + allDeps[dep] = true + } + } + + // Additionally, keep the package itself when it passes main-only check + if in.MainOnly && a.isModuleRootPackage(pkg.ImportPath) && a.shouldInclude(pkg.ImportPath, in) { + allDeps[pkg.ImportPath] = true } } + deps := make([]string, 0, len(allDeps)) for dep := range allDeps { deps = append(deps, a.shortName(dep, in.Group)) @@ -201,15 +246,36 @@ func (a *analyzer) generateDot(in Input) string { // generateJSON generates JSON output. func (a *analyzer) generateJSON(in Input) string { + result := make(map[string]any) + + // Add dependency nodes nodes := make([]*depNode, 0) for _, pkgPath := range a.getSortedPackages() { pkg := a.packages[pkgPath] - a.visited = make(map[string]bool) - node := a.buildDepNode(pkg, in, 0) - nodes = append(nodes, node) + if a.shouldInclude(pkg.ImportPath, in) { + a.visited = make(map[string]bool) + node := a.buildDepNode(pkg, in, 0) + nodes = append(nodes, node) + } } - - data, err := json.MarshalIndent(nodes, "", " ") + result["dependencies"] = nodes + + // Add statistics + result["statistics"] = a.getDependencyStats(in) + + // Add metadata + result["metadata"] = map[string]any{ + "module": a.modulePrefix, + "format": in.Format, + "depth": in.Depth, + "group": in.Group, + "internal": in.Internal, + "external": in.External, + "nostd": in.NoStd, + "main": in.MainOnly, + } + + data, err := json.MarshalIndent(result, "", " ") if err != nil { return fmt.Sprintf("Error: %v", err) } diff --git a/cmd/gf/internal/cmd/cmddep/cmddep_server.go b/cmd/gf/internal/cmd/cmddep/cmddep_server.go index 82e5acad7..e14300754 100644 --- a/cmd/gf/internal/cmd/cmddep/cmddep_server.go +++ b/cmd/gf/internal/cmd/cmddep/cmddep_server.go @@ -174,6 +174,12 @@ func (a *analyzer) handleGraphAPI(w http.ResponseWriter, r *http.Request, in Inp if i := query.Get("internal"); i != "" { in.Internal = i == "true" } + if e := query.Get("external"); e != "" { + in.External = e == "true" + } + if m := query.Get("main"); m != "" { + in.MainOnly = m == "true" + } pkg := query.Get("package") var data *graphData @@ -192,10 +198,19 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in if i := query.Get("internal"); i != "" { in.Internal = i == "true" } + if e := query.Get("external"); e != "" { + in.External = e == "true" + } + if m := query.Get("main"); m != "" { + in.MainOnly = m == "true" + } // Build reverse dependency map (who uses each package) usedByMap := make(map[string]int) for fullPath, pkg := range a.packages { + if !a.shouldInclude(fullPath, in) { + continue + } fromShort := a.shortName(fullPath, false) if fromShort == "" { continue @@ -210,6 +225,9 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in packages := make([]packageSummary, 0) for _, pkgPath := range a.getSortedPackages() { + if !a.shouldInclude(pkgPath, in) { + continue + } shortName := a.shortName(pkgPath, false) if shortName == "" { continue @@ -232,8 +250,15 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in UsedByCount: usedByMap[shortName], }) } + + // Add statistics to response + result := map[string]any{ + "packages": packages, + "statistics": a.getDependencyStats(in), + } + w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(packages) + json.NewEncoder(w).Encode(result) } // handlePackageAPI returns detailed info for a specific package. @@ -303,6 +328,12 @@ func (a *analyzer) handleTreeAPI(w http.ResponseWriter, r *http.Request, in Inpu if i := query.Get("internal"); i != "" { in.Internal = i == "true" } + if e := query.Get("external"); e != "" { + in.External = e == "true" + } + if m := query.Get("main"); m != "" { + in.MainOnly = m == "true" + } pkg := query.Get("package") var output string @@ -322,6 +353,12 @@ func (a *analyzer) handleListAPI(w http.ResponseWriter, r *http.Request, in Inpu if i := query.Get("internal"); i != "" { in.Internal = i == "true" } + if e := query.Get("external"); e != "" { + in.External = e == "true" + } + if m := query.Get("main"); m != "" { + in.MainOnly = m == "true" + } pkg := query.Get("package") var output string diff --git a/cmd/gf/internal/cmd/cmddep/static/app.js b/cmd/gf/internal/cmd/cmddep/static/app.js index f1912feea..f6a322a5b 100644 --- a/cmd/gf/internal/cmd/cmddep/static/app.js +++ b/cmd/gf/internal/cmd/cmddep/static/app.js @@ -65,6 +65,7 @@ const theme = { mermaid.initialize({ startOnLoad: false, theme: this.current === 'dark' ? 'dark' : 'default', + maxEdges: 2000, // Increase edge limit for large dependency graphs flowchart: { useMaxWidth: false, htmlLabels: true, @@ -82,6 +83,7 @@ const theme = { mermaid.initialize({ startOnLoad: false, theme: 'default', + maxEdges: 2000, // Increase edge limit for large dependency graphs flowchart: { useMaxWidth: false, htmlLabels: true, @@ -407,8 +409,26 @@ async function loadModuleName() { async function loadPackages() { try { const internal = document.getElementById('internal').checked; - const response = await fetch(`/api/packages?internal=${internal}`); - allPackages = await response.json(); + 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 data = await response.json(); + + // Handle new API response format with packages and statistics + if (data.packages && Array.isArray(data.packages)) { + allPackages = data.packages; + // Update statistics display if available + if (data.statistics) { + updateStatisticsDisplay(data.statistics); + } + } else if (Array.isArray(data)) { + // Fallback for old format + allPackages = data; + } else { + console.error('Unexpected API response format:', data); + allPackages = []; + } + document.getElementById('packageCount').textContent = allPackages.length; renderPackageList(allPackages); } catch (e) { @@ -416,6 +436,19 @@ async function loadPackages() { } } +// Update statistics display +function updateStatisticsDisplay(statistics) { + if (statistics) { + document.getElementById('internalCount').textContent = statistics.internal || 0; + document.getElementById('externalCount').textContent = statistics.external || 0; + document.getElementById('stdlibCount').textContent = statistics.stdlib || 0; + + // Update total count + const totalCount = statistics.total || (statistics.internal + statistics.external + statistics.stdlib); + document.getElementById('nodeCount').textContent = totalCount; + } +} + // Get package name from package object or string function getPkgName(pkg) { return typeof pkg === 'object' ? pkg.name : pkg; @@ -644,6 +677,8 @@ async function refresh() { const group = document.getElementById('group').checked; 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; if (selectedPackage) { await showPackageInfo(selectedPackage); @@ -652,11 +687,11 @@ async function refresh() { } if (currentView === 'graph') { - await refreshGraph(depth, group, reverse, internal); + await refreshGraph(depth, group, reverse, internal, external, mainOnly); } else if (currentView === 'tree') { - await refreshTree(depth, internal); + await refreshTree(depth, internal, external, mainOnly); } else { - await refreshList(internal); + await refreshList(internal, external, mainOnly); } } @@ -689,7 +724,7 @@ async function showPackageInfo(pkg) { } // Refresh graph view -async function refreshGraph(depth, group, reverse, internal) { +async function refreshGraph(depth, group, reverse, internal, external, mainOnly) { document.getElementById('graphView').classList.remove('hidden'); document.getElementById('textView').classList.add('hidden'); @@ -700,6 +735,12 @@ async function refreshGraph(depth, group, reverse, internal) { applyTransform(); let url = `/api/graph?depth=${depth}&group=${group}&reverse=${reverse}&internal=${internal}`; + if (external !== undefined) { + url += `&external=${external}`; + } + if (mainOnly !== undefined) { + url += `&main=${mainOnly}`; + } if (selectedPackage) { url += '&package=' + encodeURIComponent(selectedPackage); } @@ -774,11 +815,17 @@ function autoFitGraph() { } // Refresh tree view -async function refreshTree(depth, internal) { +async function refreshTree(depth, internal, external, mainOnly) { document.getElementById('graphView').classList.add('hidden'); document.getElementById('textView').classList.remove('hidden'); let url = `/api/tree?depth=${depth}&internal=${internal}`; + if (external !== undefined) { + url += `&external=${external}`; + } + if (mainOnly !== undefined) { + url += `&main=${mainOnly}`; + } if (selectedPackage) { url += '&package=' + encodeURIComponent(selectedPackage); } @@ -797,11 +844,17 @@ async function refreshTree(depth, internal) { } // Refresh list view -async function refreshList(internal) { +async function refreshList(internal, external, mainOnly) { document.getElementById('graphView').classList.add('hidden'); document.getElementById('textView').classList.remove('hidden'); let url = `/api/list?internal=${internal}`; + if (external !== undefined) { + url += `&external=${external}`; + } + if (mainOnly !== undefined) { + url += `&main=${mainOnly}`; + } if (selectedPackage) { url += '&package=' + encodeURIComponent(selectedPackage); } diff --git a/cmd/gf/internal/cmd/cmddep/static/i18n.js b/cmd/gf/internal/cmd/cmddep/static/i18n.js index 2c994a3b7..3d11687f7 100644 --- a/cmd/gf/internal/cmd/cmddep/static/i18n.js +++ b/cmd/gf/internal/cmd/cmddep/static/i18n.js @@ -23,6 +23,11 @@ const i18n = { reverseLabel: 'Reverse (show who uses)', groupLabel: 'Group by directory', internalLabel: 'Internal only', + externalLabel: 'External packages', + mainOnlyLabel: 'Main module only', + statsInternal: 'Internal', + statsExternal: 'External', + statsStdlib: 'Stdlib', layoutLabel: 'Layout:', layoutTD: 'Top-Down', layoutLR: 'Left-Right', @@ -47,7 +52,32 @@ const i18n = { latestVersion: 'Latest', errorFetchVersions: 'Failed to fetch versions', errorAnalyze: 'Failed to analyze module', - packageDetails: 'Package Details' + packageDetails: 'Package Details', + viewGraphTooltip: 'Visualize dependencies as a directed graph', + viewTreeTooltip: 'Show dependencies in a hierarchical tree structure', + viewListTooltip: 'Display dependencies as a text list', + depthTooltip: 'Limit dependency traversal depth', + reverseTooltip: 'Show reverse dependencies (who uses the selected package)', + 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)', + layoutTDTooltip: 'Top-down layout for dependency graph', + layoutLRTooltip: 'Left-right layout for dependency graph', + showAllTooltip: 'Clear package selection and show all packages', + analyzeTooltip: 'Analyze remote Go module dependencies', + resetTooltip: 'Reset to local module analysis', + themeTooltip: 'Toggle light/dark theme', + langTooltip: 'Select language', + flatModeTooltip: 'Flat list view', + treeModeTooltip: 'Tree view', + zoomInTooltip: 'Zoom in', + zoomOutTooltip: 'Zoom out', + fitToScreenTooltip: 'Fit to screen', + resetZoomTooltip: 'Reset zoom', + remoteModuleTooltip: 'Enter a remote Go module path to analyze', + versionSelectTooltip: 'Select module version (optional)', + searchTooltip: 'Search packages by name' }, zh: { title: 'Go 包依赖分析', @@ -69,6 +99,11 @@ const i18n = { reverseLabel: '反向依赖 (谁引用了它)', groupLabel: '按目录分组', internalLabel: '仅内部包', + externalLabel: '外部依赖包', + mainOnlyLabel: '仅主模块', + statsInternal: '内部', + statsExternal: '外部', + statsStdlib: '标准库', layoutLabel: '布局:', layoutTD: '从上到下', layoutLR: '从左到右', @@ -93,7 +128,32 @@ const i18n = { latestVersion: '最新版本', errorFetchVersions: '获取版本失败', errorAnalyze: '分析模块失败', - packageDetails: '包详情' + packageDetails: '包详情', + viewGraphTooltip: '以有向图形式可视化依赖关系', + viewTreeTooltip: '以层次树结构显示依赖关系', + viewListTooltip: '以文本列表形式显示依赖关系', + depthTooltip: '限制依赖遍历的深度', + reverseTooltip: '显示反向依赖(谁引用了选中的包)', + groupTooltip: '按目录结构分组显示包', + internalTooltip: '仅显示当前模块的内部包', + externalTooltip: '包含外部依赖包', + mainOnlyTooltip: '仅显示主模块包(排除vendor目录)', + layoutTDTooltip: '依赖图从上到下布局', + layoutLRTooltip: '依赖图从左到右布局', + showAllTooltip: '清除包选择,显示所有包', + analyzeTooltip: '分析远程Go模块依赖', + resetTooltip: '重置为本地模块分析', + themeTooltip: '切换明暗主题', + langTooltip: '选择语言', + flatModeTooltip: '平铺列表视图', + treeModeTooltip: '树形视图', + zoomInTooltip: '放大', + zoomOutTooltip: '缩小', + fitToScreenTooltip: '适应屏幕', + resetZoomTooltip: '重置缩放', + remoteModuleTooltip: '输入远程Go模块路径进行分析', + versionSelectTooltip: '选择模块版本(可选)', + searchTooltip: '按名称搜索包' } }, @@ -147,6 +207,12 @@ const i18n = { el.placeholder = this.t(key); }); + // Update title attributes + document.querySelectorAll('[data-i18n-title]').forEach(el => { + const key = el.getAttribute('data-i18n-title'); + el.title = this.t(key); + }); + // Update page title document.title = this.t('title'); } diff --git a/cmd/gf/internal/cmd/cmddep/static/index.html b/cmd/gf/internal/cmd/cmddep/static/index.html index 0f95e9d02..1c52ea8b6 100644 --- a/cmd/gf/internal/cmd/cmddep/static/index.html +++ b/cmd/gf/internal/cmd/cmddep/static/index.html @@ -19,24 +19,24 @@