-
Notifications
You must be signed in to change notification settings - Fork 228
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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
- Loading branch information
1 parent
0bc66cb
commit 8bfe8b3
Showing
4 changed files
with
211 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -33,7 +35,8 @@ func TestPHPDetector_Relevance(t *testing.T) { | |
}`), | ||
}, | ||
}, | ||
expected: 1, | ||
expected: 1, | ||
expectedPackages: []string{"[email protected]"}, | ||
}, | ||
} | ||
|
||
|
@@ -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", | ||
|
@@ -72,7 +82,8 @@ func TestPHPDetector_Packages(t *testing.T) { | |
}`), | ||
}, | ||
}, | ||
expectedPHP: "php@latest", | ||
expectedPHP: "php@latest", | ||
expectedPackages: []string{"php@latest"}, | ||
}, | ||
{ | ||
name: "specific php version", | ||
|
@@ -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", | ||
|
@@ -98,7 +110,8 @@ func TestPHPDetector_Packages(t *testing.T) { | |
}`), | ||
}, | ||
}, | ||
expectedPHP: "[email protected]", | ||
expectedPHP: "[email protected]", | ||
expectedPackages: []string{"[email protected]"}, | ||
}, | ||
{ | ||
name: "invalid composer.json", | ||
|
@@ -107,7 +120,8 @@ func TestPHPDetector_Packages(t *testing.T) { | |
Data: []byte(`invalid json`), | ||
}, | ||
}, | ||
expectedError: true, | ||
expectedError: true, | ||
expectedPackages: nil, | ||
}, | ||
} | ||
|
||
|
@@ -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) | ||
}) | ||
} | ||
} | ||
|
@@ -139,6 +153,7 @@ func TestPHPDetector_PHPExtensions(t *testing.T) { | |
name string | ||
fs fstest.MapFS | ||
expectedExtensions []string | ||
expectedPackages []string | ||
}{ | ||
{ | ||
name: "no extensions", | ||
|
@@ -152,6 +167,7 @@ func TestPHPDetector_PHPExtensions(t *testing.T) { | |
}, | ||
}, | ||
expectedExtensions: []string{}, | ||
expectedPackages: []string{"[email protected]"}, | ||
}, | ||
{ | ||
name: "multiple extensions", | ||
|
@@ -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", | ||
}, | ||
}, | ||
} | ||
|
||
|
@@ -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) | ||
}) | ||
} | ||
} |