Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions internal/check/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ var pipCheckRunner = func(pipBin string) error {
}

type DepsCheck struct {
Dir string
Stack string // "node", "python", or "go"
Dir string
Stack string // "node", "python", or "go"
PackageManager string // "npm", "pnpm", or "yarn" (Node only; defaults to "npm")
goCheck func(dir string) error
pipFreeze func(pipBin string) ([]byte, error)
pipCheck func(pipBin string) error
Expand Down Expand Up @@ -74,11 +75,15 @@ func (c *DepsCheck) runNode() Result {
Message: "node_modules directory exists",
}
}
pm := c.PackageManager
if pm == "" {
pm = "npm"
}
return Result{
Name: c.Name(),
Status: StatusFail,
Message: "node_modules directory not found",
Fix: "run `npm install` or `pnpm install` to install Node dependencies",
Fix: fmt.Sprintf("run `%s install` to install Node dependencies", pm),
}
}

Expand Down
36 changes: 36 additions & 0 deletions internal/check/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,42 @@ func TestDepsCheck_Node_PassAndFail(t *testing.T) {
}
}

func TestDepsCheck_Node_FixMessage_DefaultsToNpm(t *testing.T) {
dir := t.TempDir()
check := &DepsCheck{Dir: dir, Stack: "node"} // no PackageManager set
result := check.Run(context.Background())
if result.Status != StatusFail {
t.Fatalf("expected fail, got %v", result.Status)
}
if !strings.Contains(result.Fix, "npm install") {
t.Errorf("expected fix to mention 'npm install', got: %s", result.Fix)
}
}

func TestDepsCheck_Node_FixMessage_Pnpm(t *testing.T) {
dir := t.TempDir()
check := &DepsCheck{Dir: dir, Stack: "node", PackageManager: "pnpm"}
result := check.Run(context.Background())
if result.Status != StatusFail {
t.Fatalf("expected fail, got %v", result.Status)
}
if !strings.Contains(result.Fix, "pnpm install") {
t.Errorf("expected fix to mention 'pnpm install', got: %s", result.Fix)
}
}

func TestDepsCheck_Node_FixMessage_Yarn(t *testing.T) {
dir := t.TempDir()
check := &DepsCheck{Dir: dir, Stack: "node", PackageManager: "yarn"}
result := check.Run(context.Background())
if result.Status != StatusFail {
t.Fatalf("expected fail, got %v", result.Status)
}
if !strings.Contains(result.Fix, "yarn install") {
t.Errorf("expected fix to mention 'yarn install', got: %s", result.Fix)
}
}

func TestDepsCheck_Python_PassAndFail(t *testing.T) {
dir := t.TempDir()
check := &DepsCheck{Dir: dir, Stack: "python"}
Expand Down
8 changes: 6 additions & 2 deletions internal/check/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ func Build(stack detector.DetectedStack) []Check {
cs = append(cs, &DepsCheck{Dir: ".", Stack: "go"})
}
if stack.Node {
pm := stack.PackageManager
if pm == "" {
pm = "npm"
}
cs = append(cs, &BinaryCheck{Binary: "node"})
cs = append(cs, &BinaryCheck{Binary: "npm"})
cs = append(cs, &BinaryCheck{Binary: pm})
cs = append(cs, &NodeVersionCheck{Dir: "."})
cs = append(cs, &DepsCheck{Dir: ".", Stack: "node"})
cs = append(cs, &DepsCheck{Dir: ".", Stack: "node", PackageManager: pm})
cs = append(cs, &GitHooksCheck{Dir: ".", Stack: "node"})
}
if stack.Python {
Expand Down
13 changes: 13 additions & 0 deletions internal/detector/detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
type DetectedStack struct {
Go bool
Node bool
// PackageManager is the Node package manager inferred from the lockfile.
// Possible values: "npm", "pnpm", "yarn". Empty string when Node is false.
PackageManager string
Python bool
Java bool
Maven bool
Expand All @@ -27,6 +30,16 @@ func Detect(dir string) DetectedStack {

stack.Go = fileExists(filepath.Join(dir, "go.mod"))
stack.Node = fileExists(filepath.Join(dir, "package.json"))
if stack.Node {
switch {
case fileExists(filepath.Join(dir, "pnpm-lock.yaml")):
stack.PackageManager = "pnpm"
case fileExists(filepath.Join(dir, "yarn.lock")):
stack.PackageManager = "yarn"
default:
stack.PackageManager = "npm"
}
}
stack.Python = fileExists(filepath.Join(dir, "requirements.txt")) ||
fileExists(filepath.Join(dir, "pyproject.toml"))
stack.Maven = fileExists(filepath.Join(dir, "pom.xml"))
Expand Down
71 changes: 71 additions & 0 deletions internal/detector/detector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package detector

import (
"os"
"path/filepath"
"testing"
)

func TestDetect_PackageManager_npm(t *testing.T) {
dir := t.TempDir()
touch(t, filepath.Join(dir, "package.json"))
// no lockfile → defaults to npm
stack := Detect(dir)
if !stack.Node {
t.Fatal("expected Node to be detected")
}
if stack.PackageManager != "npm" {
t.Errorf("expected PackageManager=npm, got %q", stack.PackageManager)
}
}

func TestDetect_PackageManager_pnpm(t *testing.T) {
dir := t.TempDir()
touch(t, filepath.Join(dir, "package.json"))
touch(t, filepath.Join(dir, "pnpm-lock.yaml"))
stack := Detect(dir)
if stack.PackageManager != "pnpm" {
t.Errorf("expected PackageManager=pnpm, got %q", stack.PackageManager)
}
}

func TestDetect_PackageManager_yarn(t *testing.T) {
dir := t.TempDir()
touch(t, filepath.Join(dir, "package.json"))
touch(t, filepath.Join(dir, "yarn.lock"))
stack := Detect(dir)
if stack.PackageManager != "yarn" {
t.Errorf("expected PackageManager=yarn, got %q", stack.PackageManager)
}
}

func TestDetect_PackageManager_pnpm_takes_priority_over_yarn(t *testing.T) {
// If somehow both lockfiles exist, pnpm wins (checked first).
dir := t.TempDir()
touch(t, filepath.Join(dir, "package.json"))
touch(t, filepath.Join(dir, "pnpm-lock.yaml"))
touch(t, filepath.Join(dir, "yarn.lock"))
stack := Detect(dir)
if stack.PackageManager != "pnpm" {
t.Errorf("expected PackageManager=pnpm when both lockfiles exist, got %q", stack.PackageManager)
}
}

func TestDetect_PackageManager_empty_when_not_node(t *testing.T) {
dir := t.TempDir()
// no package.json
stack := Detect(dir)
if stack.Node {
t.Fatal("did not expect Node to be detected")
}
if stack.PackageManager != "" {
t.Errorf("expected PackageManager to be empty for non-Node project, got %q", stack.PackageManager)
}
}

func touch(t *testing.T, path string) {
t.Helper()
if err := os.WriteFile(path, nil, 0o644); err != nil {
t.Fatalf("touch %s: %v", path, err)
}
}
Loading