Skip to content

Commit 8bfe8b3

Browse files
authored
[zeroconfig] Implement nodejs detector (#2395)
## 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
1 parent 0bc66cb commit 8bfe8b3

File tree

4 files changed

+211
-16
lines changed

4 files changed

+211
-16
lines changed

pkg/autodetect/autodetect.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func populateConfig(ctx context.Context, path string, config *devconfig.Config)
4242
func detectors(path string) []detector.Detector {
4343
return []detector.Detector{
4444
&detector.GoDetector{Root: path},
45+
&detector.NodeJSDetector{Root: path},
4546
&detector.PHPDetector{Root: path},
4647
&detector.PoetryDetector{Root: path},
4748
&detector.PythonDetector{Root: path},

pkg/autodetect/detector/nodejs.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package detector
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"os"
7+
"path/filepath"
8+
"regexp"
9+
)
10+
11+
type packageJSON struct {
12+
Engines struct {
13+
Node string `json:"node"`
14+
} `json:"engines"`
15+
}
16+
17+
type NodeJSDetector struct {
18+
Root string
19+
packageJSON *packageJSON
20+
}
21+
22+
var _ Detector = &NodeJSDetector{}
23+
24+
func (d *NodeJSDetector) Init() error {
25+
pkgJSON, err := loadPackageJSON(d.Root)
26+
if err != nil && !os.IsNotExist(err) {
27+
return err
28+
}
29+
d.packageJSON = pkgJSON
30+
return nil
31+
}
32+
33+
func (d *NodeJSDetector) Relevance(path string) (float64, error) {
34+
if d.packageJSON == nil {
35+
return 0, nil
36+
}
37+
return 1, nil
38+
}
39+
40+
func (d *NodeJSDetector) Packages(ctx context.Context) ([]string, error) {
41+
return []string{"nodejs@" + d.nodeVersion(ctx)}, nil
42+
}
43+
44+
func (d *NodeJSDetector) nodeVersion(ctx context.Context) string {
45+
if d.packageJSON == nil || d.packageJSON.Engines.Node == "" {
46+
return "latest" // Default to latest if not specified
47+
}
48+
49+
// Remove any non-semver characters (e.g. ">=", "^", etc)
50+
version := "latest"
51+
semverRegex := regexp.MustCompile(`\d+(\.\d+)?(\.\d+)?`)
52+
if match := semverRegex.FindString(d.packageJSON.Engines.Node); match != "" {
53+
version = match
54+
}
55+
56+
return determineBestVersion(ctx, "nodejs", version)
57+
}
58+
59+
func loadPackageJSON(root string) (*packageJSON, error) {
60+
path := filepath.Join(root, "package.json")
61+
data, err := os.ReadFile(path)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
var pkg packageJSON
67+
if err := json.Unmarshal(data, &pkg); err != nil {
68+
return nil, err
69+
}
70+
return &pkg, nil
71+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package detector
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
"testing/fstest"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestNodeJSDetector_Relevance(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
fs fstest.MapFS
18+
expected float64
19+
expectedPackages []string
20+
}{
21+
{
22+
name: "package.json in root",
23+
fs: fstest.MapFS{
24+
"package.json": &fstest.MapFile{
25+
Data: []byte(`{}`),
26+
},
27+
},
28+
expected: 1,
29+
expectedPackages: []string{"nodejs@latest"},
30+
},
31+
{
32+
name: "package.json with node version",
33+
fs: fstest.MapFS{
34+
"package.json": &fstest.MapFile{
35+
Data: []byte(`{
36+
"engines": {
37+
"node": ">=18.0.0"
38+
}
39+
}`),
40+
},
41+
},
42+
expected: 1,
43+
expectedPackages: []string{"[email protected]"},
44+
},
45+
{
46+
name: "no nodejs files",
47+
fs: fstest.MapFS{
48+
"main.py": &fstest.MapFile{
49+
Data: []byte(``),
50+
},
51+
"requirements.txt": &fstest.MapFile{
52+
Data: []byte(``),
53+
},
54+
},
55+
expected: 0,
56+
expectedPackages: []string{},
57+
},
58+
{
59+
name: "empty directory",
60+
fs: fstest.MapFS{},
61+
expected: 0,
62+
expectedPackages: []string{},
63+
},
64+
}
65+
66+
for _, curTest := range tests {
67+
t.Run(curTest.name, func(t *testing.T) {
68+
dir := t.TempDir()
69+
for name, file := range curTest.fs {
70+
fullPath := filepath.Join(dir, name)
71+
err := os.MkdirAll(filepath.Dir(fullPath), 0o755)
72+
require.NoError(t, err)
73+
err = os.WriteFile(fullPath, file.Data, 0o644)
74+
require.NoError(t, err)
75+
}
76+
77+
d := &NodeJSDetector{Root: dir}
78+
err := d.Init()
79+
require.NoError(t, err)
80+
81+
score, err := d.Relevance(dir)
82+
require.NoError(t, err)
83+
assert.Equal(t, curTest.expected, score)
84+
if score > 0 {
85+
packages, err := d.Packages(context.Background())
86+
require.NoError(t, err)
87+
assert.Equal(t, curTest.expectedPackages, packages)
88+
}
89+
})
90+
}
91+
}
92+
93+
func TestNodeJSDetector_Packages(t *testing.T) {
94+
d := &NodeJSDetector{}
95+
packages, err := d.Packages(context.Background())
96+
require.NoError(t, err)
97+
assert.Equal(t, []string{"nodejs@latest"}, packages)
98+
}

pkg/autodetect/detector/php_test.go

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ import (
1313

1414
func TestPHPDetector_Relevance(t *testing.T) {
1515
tests := []struct {
16-
name string
17-
fs fstest.MapFS
18-
expected float64
16+
name string
17+
fs fstest.MapFS
18+
expected float64
19+
expectedPackages []string
1920
}{
2021
{
21-
name: "no composer.json",
22-
fs: fstest.MapFS{},
23-
expected: 0,
22+
name: "no composer.json",
23+
fs: fstest.MapFS{},
24+
expected: 0,
25+
expectedPackages: nil,
2426
},
2527
{
2628
name: "with composer.json",
@@ -33,7 +35,8 @@ func TestPHPDetector_Relevance(t *testing.T) {
3335
}`),
3436
},
3537
},
36-
expected: 1,
38+
expected: 1,
39+
expectedPackages: []string{"[email protected]"},
3740
},
3841
}
3942

@@ -52,16 +55,23 @@ func TestPHPDetector_Relevance(t *testing.T) {
5255
score, err := d.Relevance(dir)
5356
require.NoError(t, err)
5457
assert.Equal(t, curTest.expected, score)
58+
59+
if score > 0 {
60+
packages, err := d.Packages(context.Background())
61+
require.NoError(t, err)
62+
assert.Equal(t, curTest.expectedPackages, packages)
63+
}
5564
})
5665
}
5766
}
5867

5968
func TestPHPDetector_Packages(t *testing.T) {
6069
tests := []struct {
61-
name string
62-
fs fstest.MapFS
63-
expectedPHP string
64-
expectedError bool
70+
name string
71+
fs fstest.MapFS
72+
expectedPHP string
73+
expectedError bool
74+
expectedPackages []string
6575
}{
6676
{
6777
name: "no php version specified",
@@ -72,7 +82,8 @@ func TestPHPDetector_Packages(t *testing.T) {
7282
}`),
7383
},
7484
},
75-
expectedPHP: "php@latest",
85+
expectedPHP: "php@latest",
86+
expectedPackages: []string{"php@latest"},
7687
},
7788
{
7889
name: "specific php version",
@@ -85,7 +96,8 @@ func TestPHPDetector_Packages(t *testing.T) {
8596
}`),
8697
},
8798
},
88-
expectedPHP: "[email protected]",
99+
expectedPHP: "[email protected]",
100+
expectedPackages: []string{"[email protected]"},
89101
},
90102
{
91103
name: "php version with patch",
@@ -98,7 +110,8 @@ func TestPHPDetector_Packages(t *testing.T) {
98110
}`),
99111
},
100112
},
101-
expectedPHP: "[email protected]",
113+
expectedPHP: "[email protected]",
114+
expectedPackages: []string{"[email protected]"},
102115
},
103116
{
104117
name: "invalid composer.json",
@@ -107,7 +120,8 @@ func TestPHPDetector_Packages(t *testing.T) {
107120
Data: []byte(`invalid json`),
108121
},
109122
},
110-
expectedError: true,
123+
expectedError: true,
124+
expectedPackages: nil,
111125
},
112126
}
113127

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

130144
packages, err := d.Packages(context.Background())
131145
require.NoError(t, err)
132-
assert.Equal(t, []string{curTest.expectedPHP}, packages)
146+
assert.Equal(t, curTest.expectedPackages, packages)
133147
})
134148
}
135149
}
@@ -139,6 +153,7 @@ func TestPHPDetector_PHPExtensions(t *testing.T) {
139153
name string
140154
fs fstest.MapFS
141155
expectedExtensions []string
156+
expectedPackages []string
142157
}{
143158
{
144159
name: "no extensions",
@@ -152,6 +167,7 @@ func TestPHPDetector_PHPExtensions(t *testing.T) {
152167
},
153168
},
154169
expectedExtensions: []string{},
170+
expectedPackages: []string{"[email protected]"},
155171
},
156172
{
157173
name: "multiple extensions",
@@ -170,6 +186,11 @@ func TestPHPDetector_PHPExtensions(t *testing.T) {
170186
"php81Extensions.mbstring@latest",
171187
"php81Extensions.imagick@latest",
172188
},
189+
expectedPackages: []string{
190+
191+
"php81Extensions.mbstring@latest",
192+
"php81Extensions.imagick@latest",
193+
},
173194
},
174195
}
175196

@@ -188,6 +209,10 @@ func TestPHPDetector_PHPExtensions(t *testing.T) {
188209
extensions, err := d.phpExtensions(context.Background())
189210
require.NoError(t, err)
190211
assert.ElementsMatch(t, curTest.expectedExtensions, extensions)
212+
213+
packages, err := d.Packages(context.Background())
214+
require.NoError(t, err)
215+
assert.ElementsMatch(t, curTest.expectedPackages, packages)
191216
})
192217
}
193218
}

0 commit comments

Comments
 (0)