Skip to content

Commit

Permalink
[zeroconfig] Implement nodejs detector (#2395)
Browse files Browse the repository at this point in the history
## Summary

TSIA

Possible improvement: should we default `DEVBOX_COREPACK_ENABLED` to
true? (cc: @LucilleH )

## How was it tested?

- [x] unit tests
- [x] Manually created directory with package.json
  • Loading branch information
mikeland73 authored Oct 30, 2024
1 parent 0bc66cb commit 8bfe8b3
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 16 deletions.
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)
})
}
}

0 comments on commit 8bfe8b3

Please sign in to comment.