Skip to content

Commit 396d4d5

Browse files
authored
This PR refactors package manager detection for Node.js and Python to eliminate code duplication and provide better traceability of how package managers are detected. (#50)
* Enhance package manager detection and prioritize package.json field over lock files * Refactor package manager detection and improve test coverage * Enhance package manager detection and add go vet command; improve test coverage and documentation * Fix logging for invalid JSON in package.json and update comment for Windows taskkill * Refactor package manager detection to consolidate logic and improve source tracking; enhance health command tests for clarity and reliability * Refactor package manager detection to include pipenv support and remove unused readFileContent function
1 parent 1dee2d9 commit 396d4d5

File tree

20 files changed

+802
-156
lines changed

20 files changed

+802
-156
lines changed

cli/magefile.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,16 @@ func Fmt() error {
267267
return nil
268268
}
269269

270+
// Vet runs go vet to check for suspicious constructs.
271+
func Vet() error {
272+
fmt.Println("Running go vet...")
273+
if err := sh.RunV("go", "vet", "./..."); err != nil {
274+
return fmt.Errorf("go vet found issues: %w", err)
275+
}
276+
fmt.Println("✅ go vet passed!")
277+
return nil
278+
}
279+
270280
// Clean removes build artifacts and coverage reports.
271281
func Clean() error {
272282
fmt.Println("Cleaning build artifacts...")
@@ -355,6 +365,7 @@ func Preflight() error {
355365
{"Building and linting dashboard", DashboardBuild},
356366
{"Running dashboard tests", DashboardTest},
357367
{"Building Go binary", Build},
368+
{"Running go vet", Vet},
358369
{"Running standard linting", Lint},
359370
{"Running quick security scan", runQuickSecurity},
360371
{"Running all tests with coverage", TestCoverage},

cli/src/cmd/app/commands/generate.go

Lines changed: 22 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -257,158 +257,60 @@ func hasGit(dir string) bool {
257257

258258
// Tool detection functions
259259
func detectNode(_ string) DetectedRequirement {
260-
req := DetectedRequirement{
261-
Name: "node",
262-
Source: "package.json",
263-
}
264-
265-
installedVersion, err := getToolVersion("node")
266-
if err != nil {
267-
return req
268-
}
269-
270-
req.InstalledVersion = installedVersion
271-
req.MinVersion = normalizeVersion(installedVersion, "node")
272-
return req
260+
return detectToolWithSource("node", "package.json", false)
273261
}
274262

275263
func detectNodePackageManager(projectDir string) DetectedRequirement {
276-
// Priority: pnpm > yarn > npm
277-
if fileExists(projectDir, "pnpm-lock.yaml") || fileExists(projectDir, "pnpm-workspace.yaml") {
278-
return detectTool("pnpm", "pnpm-lock.yaml")
279-
}
280-
if fileExists(projectDir, "yarn.lock") {
281-
return detectTool("yarn", "yarn.lock")
282-
}
283-
if fileExists(projectDir, "package-lock.json") {
284-
return detectTool("npm", "package-lock.json")
285-
}
286-
// Default to npm
287-
return detectTool("npm", "package.json")
264+
// Use detector to get both package manager and source in one call
265+
info := detector.DetectNodePackageManagerWithSource(projectDir)
266+
return detectTool(info.Name, info.Source)
288267
}
289268

290269
func detectPython(_ string) DetectedRequirement {
291-
req := DetectedRequirement{
292-
Name: "python",
293-
Source: "requirements.txt or pyproject.toml",
294-
}
295-
296-
installedVersion, err := getToolVersion("python")
297-
if err != nil {
298-
return req
299-
}
300-
301-
req.InstalledVersion = installedVersion
302-
req.MinVersion = normalizeVersion(installedVersion, "python")
303-
return req
270+
return detectToolWithSource("python", "requirements.txt or pyproject.toml", false)
304271
}
305272

306273
func detectPythonPackageManager(projectDir string) DetectedRequirement {
307-
// Priority: uv > poetry > pipenv > pip
308-
if fileExists(projectDir, "uv.lock") {
309-
return detectTool("uv", "uv.lock")
310-
}
311-
312-
if fileExists(projectDir, "pyproject.toml") {
313-
content := readFileContent(filepath.Join(projectDir, "pyproject.toml"))
314-
if strings.Contains(content, "[tool.uv]") {
315-
return detectTool("uv", "pyproject.toml")
316-
}
317-
if strings.Contains(content, "[tool.poetry]") {
318-
return detectTool("poetry", "pyproject.toml")
319-
}
320-
}
321-
322-
if fileExists(projectDir, "poetry.lock") {
323-
return detectTool("poetry", "poetry.lock")
324-
}
325-
326-
if fileExists(projectDir, "Pipfile") || fileExists(projectDir, "Pipfile.lock") {
327-
return detectTool("pipenv", "Pipfile")
328-
}
329-
330-
// Default to pip
331-
return detectTool("pip", "requirements.txt")
274+
// Use detector to get both package manager and source in one call
275+
info := detector.DetectPythonPackageManagerWithSource(projectDir)
276+
return detectTool(info.Name, info.Source)
332277
}
333278

334279
func detectDotnet(_ string) DetectedRequirement {
335-
req := DetectedRequirement{
336-
Name: "dotnet",
337-
Source: ".csproj or .sln",
338-
}
339-
340-
installedVersion, err := getToolVersion("dotnet")
341-
if err != nil {
342-
return req
343-
}
344-
345-
req.InstalledVersion = installedVersion
346-
req.MinVersion = normalizeVersion(installedVersion, "dotnet")
347-
return req
280+
return detectToolWithSource("dotnet", ".csproj or .sln", false)
348281
}
349282

350283
func detectAspire(_ string) DetectedRequirement {
351-
req := DetectedRequirement{
352-
Name: "aspire",
353-
Source: "AppHost.cs",
354-
}
355-
356-
installedVersion, err := getToolVersion("aspire")
357-
if err != nil {
358-
return req
359-
}
360-
361-
req.InstalledVersion = installedVersion
362-
req.MinVersion = normalizeVersion(installedVersion, "aspire")
363-
return req
284+
return detectToolWithSource("aspire", "AppHost.cs", false)
364285
}
365286

366287
func detectDocker(_ string) DetectedRequirement {
367-
req := DetectedRequirement{
368-
Name: "docker",
369-
Source: "Dockerfile or docker-compose.yml",
370-
CheckRunning: true,
371-
}
372-
373-
installedVersion, err := getToolVersion("docker")
374-
if err != nil {
375-
return req
376-
}
377-
378-
req.InstalledVersion = installedVersion
379-
req.MinVersion = normalizeVersion(installedVersion, "docker")
380-
return req
288+
return detectToolWithSource("docker", "Dockerfile or docker-compose.yml", true)
381289
}
382290

383291
func detectAzd(_ string) DetectedRequirement {
384-
req := DetectedRequirement{
385-
Name: "azd",
386-
Source: "azure.yaml",
387-
}
388-
389-
installedVersion, err := getToolVersion("azd")
390-
if err != nil {
391-
return req
392-
}
393-
394-
req.InstalledVersion = installedVersion
395-
req.MinVersion = normalizeVersion(installedVersion, "azd")
396-
return req
292+
return detectToolWithSource("azd", "azure.yaml", false)
397293
}
398294

399295
func detectGit(_ string) DetectedRequirement {
296+
return detectToolWithSource("git", ".git directory", false)
297+
}
298+
299+
// detectToolWithSource is a helper for detecting tools with specified source and checkRunning flag.
300+
func detectToolWithSource(toolName, source string, checkRunning bool) DetectedRequirement {
400301
req := DetectedRequirement{
401-
Name: "git",
402-
Source: ".git directory",
302+
Name: toolName,
303+
Source: source,
304+
CheckRunning: checkRunning,
403305
}
404306

405-
installedVersion, err := getToolVersion("git")
307+
installedVersion, err := getToolVersion(toolName)
406308
if err != nil {
407309
return req
408310
}
409311

410312
req.InstalledVersion = installedVersion
411-
req.MinVersion = normalizeVersion(installedVersion, "git")
313+
req.MinVersion = normalizeVersion(installedVersion, toolName)
412314
return req
413315
}
414316

@@ -533,18 +435,6 @@ func fileExists(dir, filename string) bool {
533435
return err == nil
534436
}
535437

536-
func readFileContent(path string) string {
537-
if err := security.ValidatePath(path); err != nil {
538-
return ""
539-
}
540-
// #nosec G304 -- Path validated by security.ValidatePath
541-
data, err := os.ReadFile(path)
542-
if err != nil {
543-
return ""
544-
}
545-
return string(data)
546-
}
547-
548438
// Display functions
549439
func displayDetectedDependencies(requirements []DetectedRequirement) {
550440
sources := make(map[string]bool)

cli/src/cmd/app/commands/generate_test.go

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,45 @@ func TestDetectNodePackageManager(t *testing.T) {
850850
expectedID string
851851
expectedSource string
852852
}{
853+
{
854+
name: "packageManager field takes priority over lock files - pnpm",
855+
setup: func(dir string) error {
856+
pkgJSON := `{"name": "test", "packageManager": "pnpm@8.15.0"}`
857+
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(pkgJSON), 0600); err != nil {
858+
return err
859+
}
860+
// Create yarn.lock to test priority
861+
return os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0600)
862+
},
863+
expectedID: "pnpm",
864+
expectedSource: "package.json (packageManager field)",
865+
},
866+
{
867+
name: "packageManager field takes priority over lock files - yarn",
868+
setup: func(dir string) error {
869+
pkgJSON := `{"name": "test", "packageManager": "yarn@4.1.0"}`
870+
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(pkgJSON), 0600); err != nil {
871+
return err
872+
}
873+
// Create pnpm-lock.yaml to test priority
874+
return os.WriteFile(filepath.Join(dir, "pnpm-lock.yaml"), []byte(""), 0600)
875+
},
876+
expectedID: "yarn",
877+
expectedSource: "package.json (packageManager field)",
878+
},
879+
{
880+
name: "packageManager field takes priority over lock files - npm",
881+
setup: func(dir string) error {
882+
pkgJSON := `{"name": "test", "packageManager": "npm@10.5.0"}`
883+
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(pkgJSON), 0600); err != nil {
884+
return err
885+
}
886+
// Create yarn.lock to test priority
887+
return os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0600)
888+
},
889+
expectedID: "npm",
890+
expectedSource: "package.json (packageManager field)",
891+
},
853892
{
854893
name: "detects pnpm from lock file",
855894
setup: func(dir string) error {
@@ -864,7 +903,7 @@ func TestDetectNodePackageManager(t *testing.T) {
864903
return os.WriteFile(filepath.Join(dir, "pnpm-workspace.yaml"), []byte(""), 0600)
865904
},
866905
expectedID: "pnpm",
867-
expectedSource: "pnpm-lock.yaml",
906+
expectedSource: "pnpm-workspace.yaml",
868907
},
869908
{
870909
name: "detects yarn from lock file",
@@ -888,6 +927,30 @@ func TestDetectNodePackageManager(t *testing.T) {
888927
expectedID: "npm",
889928
expectedSource: "package.json",
890929
},
930+
{
931+
name: "unsupported package manager in packageManager field falls back to lock files",
932+
setup: func(dir string) error {
933+
// Set packageManager to an unsupported manager (e.g., "bun")
934+
pkgJSON := `{"name": "test", "packageManager": "bun@1.0.0"}`
935+
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(pkgJSON), 0600); err != nil {
936+
return err
937+
}
938+
// Create pnpm-lock.yaml - should fall back to this
939+
return os.WriteFile(filepath.Join(dir, "pnpm-lock.yaml"), []byte(""), 0600)
940+
},
941+
expectedID: "pnpm",
942+
expectedSource: "pnpm-lock.yaml",
943+
},
944+
{
945+
name: "unsupported package manager with no lock files defaults to npm",
946+
setup: func(dir string) error {
947+
// Set packageManager to an unsupported manager with no lock files
948+
pkgJSON := `{"name": "test", "packageManager": "bun@1.0.0"}`
949+
return os.WriteFile(filepath.Join(dir, "package.json"), []byte(pkgJSON), 0600)
950+
},
951+
expectedID: "npm",
952+
expectedSource: "package.json",
953+
},
891954
}
892955

893956
for _, tt := range tests {

0 commit comments

Comments
 (0)