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
1 change: 1 addition & 0 deletions pkg/autodetect/autodetect.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func populateConfig(ctx context.Context, path string, config *devconfig.Config)
func detectors(path string) []detector.Detector {
return []detector.Detector{
&detector.GoDetector{Root: path},
&detector.NodeJSDetector{Root: path},
&detector.PHPDetector{Root: path},
&detector.PoetryDetector{Root: path},
&detector.PythonDetector{Root: path},
Expand Down
71 changes: 71 additions & 0 deletions pkg/autodetect/detector/nodejs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package detector

import (
"context"
"encoding/json"
"os"
"path/filepath"
"regexp"
)

type packageJSON struct {
Engines struct {
Node string `json:"node"`
} `json:"engines"`
}

type NodeJSDetector struct {
Root string
packageJSON *packageJSON
}

var _ Detector = &NodeJSDetector{}

func (d *NodeJSDetector) Init() error {
pkgJSON, err := loadPackageJSON(d.Root)
if err != nil && !os.IsNotExist(err) {
return err
}
d.packageJSON = pkgJSON
return nil
}

func (d *NodeJSDetector) Relevance(path string) (float64, error) {
if d.packageJSON == nil {
return 0, nil
}
return 1, nil
}

func (d *NodeJSDetector) Packages(ctx context.Context) ([]string, error) {
return []string{"nodejs@" + d.nodeVersion(ctx)}, nil
}

func (d *NodeJSDetector) nodeVersion(ctx context.Context) string {
if d.packageJSON == nil || d.packageJSON.Engines.Node == "" {
return "latest" // Default to latest if not specified
}

// Remove any non-semver characters (e.g. ">=", "^", etc)
version := "latest"
semverRegex := regexp.MustCompile(`\d+(\.\d+)?(\.\d+)?`)
if match := semverRegex.FindString(d.packageJSON.Engines.Node); match != "" {
version = match
}

return determineBestVersion(ctx, "nodejs", version)
}

func loadPackageJSON(root string) (*packageJSON, error) {
path := filepath.Join(root, "package.json")
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}

var pkg packageJSON
if err := json.Unmarshal(data, &pkg); err != nil {
return nil, err
}
return &pkg, nil
}
98 changes: 98 additions & 0 deletions pkg/autodetect/detector/nodejs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package detector

import (
"context"
"os"
"path/filepath"
"testing"
"testing/fstest"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNodeJSDetector_Relevance(t *testing.T) {
tests := []struct {
name string
fs fstest.MapFS
expected float64
expectedPackages []string
}{
{
name: "package.json in root",
fs: fstest.MapFS{
"package.json": &fstest.MapFile{
Data: []byte(`{}`),
},
},
expected: 1,
expectedPackages: []string{"nodejs@latest"},
},
{
name: "package.json with node version",
fs: fstest.MapFS{
"package.json": &fstest.MapFile{
Data: []byte(`{
"engines": {
"node": ">=18.0.0"
}
}`),
},
},
expected: 1,
expectedPackages: []string{"[email protected]"},
},
{
name: "no nodejs files",
fs: fstest.MapFS{
"main.py": &fstest.MapFile{
Data: []byte(``),
},
"requirements.txt": &fstest.MapFile{
Data: []byte(``),
},
},
expected: 0,
expectedPackages: []string{},
},
{
name: "empty directory",
fs: fstest.MapFS{},
expected: 0,
expectedPackages: []string{},
},
}

for _, curTest := range tests {
t.Run(curTest.name, func(t *testing.T) {
dir := t.TempDir()
for name, file := range curTest.fs {
fullPath := filepath.Join(dir, name)
err := os.MkdirAll(filepath.Dir(fullPath), 0o755)
require.NoError(t, err)
err = os.WriteFile(fullPath, file.Data, 0o644)
require.NoError(t, err)
}

d := &NodeJSDetector{Root: dir}
err := d.Init()
require.NoError(t, err)

score, err := d.Relevance(dir)
require.NoError(t, err)
assert.Equal(t, curTest.expected, score)
if score > 0 {
packages, err := d.Packages(context.Background())
require.NoError(t, err)
assert.Equal(t, curTest.expectedPackages, packages)
}
})
}
}

func TestNodeJSDetector_Packages(t *testing.T) {
d := &NodeJSDetector{}
packages, err := d.Packages(context.Background())
require.NoError(t, err)
assert.Equal(t, []string{"nodejs@latest"}, packages)
}
57 changes: 41 additions & 16 deletions pkg/autodetect/detector/php_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import (

func TestPHPDetector_Relevance(t *testing.T) {
tests := []struct {
name string
fs fstest.MapFS
expected float64
name string
fs fstest.MapFS
expected float64
expectedPackages []string
}{
{
name: "no composer.json",
fs: fstest.MapFS{},
expected: 0,
name: "no composer.json",
fs: fstest.MapFS{},
expected: 0,
expectedPackages: nil,
},
{
name: "with composer.json",
Expand All @@ -33,7 +35,8 @@ func TestPHPDetector_Relevance(t *testing.T) {
}`),
},
},
expected: 1,
expected: 1,
expectedPackages: []string{"[email protected]"},
},
}

Expand All @@ -52,16 +55,23 @@ func TestPHPDetector_Relevance(t *testing.T) {
score, err := d.Relevance(dir)
require.NoError(t, err)
assert.Equal(t, curTest.expected, score)

if score > 0 {
packages, err := d.Packages(context.Background())
require.NoError(t, err)
assert.Equal(t, curTest.expectedPackages, packages)
}
})
}
}

func TestPHPDetector_Packages(t *testing.T) {
tests := []struct {
name string
fs fstest.MapFS
expectedPHP string
expectedError bool
name string
fs fstest.MapFS
expectedPHP string
expectedError bool
expectedPackages []string
}{
{
name: "no php version specified",
Expand All @@ -72,7 +82,8 @@ func TestPHPDetector_Packages(t *testing.T) {
}`),
},
},
expectedPHP: "php@latest",
expectedPHP: "php@latest",
expectedPackages: []string{"php@latest"},
},
{
name: "specific php version",
Expand All @@ -85,7 +96,8 @@ func TestPHPDetector_Packages(t *testing.T) {
}`),
},
},
expectedPHP: "[email protected]",
expectedPHP: "[email protected]",
expectedPackages: []string{"[email protected]"},
},
{
name: "php version with patch",
Expand All @@ -98,7 +110,8 @@ func TestPHPDetector_Packages(t *testing.T) {
}`),
},
},
expectedPHP: "[email protected]",
expectedPHP: "[email protected]",
expectedPackages: []string{"[email protected]"},
},
{
name: "invalid composer.json",
Expand All @@ -107,7 +120,8 @@ func TestPHPDetector_Packages(t *testing.T) {
Data: []byte(`invalid json`),
},
},
expectedError: true,
expectedError: true,
expectedPackages: nil,
},
}

Expand All @@ -129,7 +143,7 @@ func TestPHPDetector_Packages(t *testing.T) {

packages, err := d.Packages(context.Background())
require.NoError(t, err)
assert.Equal(t, []string{curTest.expectedPHP}, packages)
assert.Equal(t, curTest.expectedPackages, packages)
})
}
}
Expand All @@ -139,6 +153,7 @@ func TestPHPDetector_PHPExtensions(t *testing.T) {
name string
fs fstest.MapFS
expectedExtensions []string
expectedPackages []string
}{
{
name: "no extensions",
Expand All @@ -152,6 +167,7 @@ func TestPHPDetector_PHPExtensions(t *testing.T) {
},
},
expectedExtensions: []string{},
expectedPackages: []string{"[email protected]"},
},
{
name: "multiple extensions",
Expand All @@ -170,6 +186,11 @@ func TestPHPDetector_PHPExtensions(t *testing.T) {
"php81Extensions.mbstring@latest",
"php81Extensions.imagick@latest",
},
expectedPackages: []string{
"[email protected]",
"php81Extensions.mbstring@latest",
"php81Extensions.imagick@latest",
},
},
}

Expand All @@ -188,6 +209,10 @@ func TestPHPDetector_PHPExtensions(t *testing.T) {
extensions, err := d.phpExtensions(context.Background())
require.NoError(t, err)
assert.ElementsMatch(t, curTest.expectedExtensions, extensions)

packages, err := d.Packages(context.Background())
require.NoError(t, err)
assert.ElementsMatch(t, curTest.expectedPackages, packages)
})
}
}
Loading