Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
eyalbe4 committed Nov 10, 2024
2 parents c9ac83b + 36ed221 commit e7739ae
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 18 deletions.
51 changes: 35 additions & 16 deletions build/utils/yarn.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,17 @@ func GetYarnExecutable() (string, error) {
return yarnExecPath, nil
}

// GetYarnDependencies returns a map of the dependencies of a Yarn project and the root package of the project.
// The keys are the packages' values (Yarn's full identifiers of the packages), for example: '@scope/[email protected]'
// (for yarn v < 2.0.0) or @scope/package-name@npm:1.0.0 (for yarn v >= 2.0.0).
// Pay attention that a package's value won't necessarily contain its version. Use the version in package's details instead.
func GetYarnDependencies(executablePath, srcPath string, packageInfo *PackageInfo, log utils.Log) (dependenciesMap map[string]*YarnDependency, root *YarnDependency, err error) {
// Returns a map of the dependencies of a Yarn project along with the root package of the project.
// The map's keys are the full identifiers of the packages as used by Yarn; for example:
// '@scope/[email protected]' (for Yarn versions < 2.0.0) or '@scope/package-name@npm:1.0.0' (for Yarn versions >= 2.0.0).
// Note that a package's value may not necessarily contain its version; instead, use the version in the package's details.
// Arguments:
// executablePath - The path to the Yarn executable.
// srcPath - The path to the project's source that we wish to work with.
// packageInfo - The project's package information.
// log - The logger.
// allowPartialResults - If true, the function will allow some errors to occur without failing the flow and will generate partial results.
func GetYarnDependencies(executablePath, srcPath string, packageInfo *PackageInfo, log utils.Log, allowPartialResults bool) (dependenciesMap map[string]*YarnDependency, root *YarnDependency, err error) {
executableVersionStr, err := GetVersion(executablePath, srcPath)
if err != nil {
return
Expand All @@ -124,7 +130,7 @@ func GetYarnDependencies(executablePath, srcPath string, packageInfo *PackageInf
if isV2AndAbove {
dependenciesMap, root, err = buildYarnV2DependencyMap(packageInfo, responseStr)
} else {
dependenciesMap, root, err = buildYarnV1DependencyMap(packageInfo, responseStr)
dependenciesMap, root, err = buildYarnV1DependencyMap(packageInfo, responseStr, allowPartialResults, log)
}
return
}
Expand All @@ -144,10 +150,15 @@ func GetVersion(executablePath, srcPath string) (string, error) {
return strings.TrimSpace(outBuffer.String()), err
}

// buildYarnV1DependencyMap builds a map of dependencies for Yarn versions < 2.0.0
// Pay attention that in Yarn < 2.0.0 the project itself with its direct dependencies is not presented when running the
// command 'yarn list' therefore the root is built manually.
func buildYarnV1DependencyMap(packageInfo *PackageInfo, responseStr string) (dependenciesMap map[string]*YarnDependency, root *YarnDependency, err error) {
// Builds a map of dependencies for Yarn versions < 2.0.0.
// Note that in Yarn < 2.0.0, the project itself, along with its direct dependencies, is not present when running the
// command 'yarn list'; therefore, the root is built manually.
// Arguments:
// packageInfo - The project's package information.
// responseStr - The response string from the 'yarn list' command.
// allowPartialResults - If true, the function will allow some errors to occur without failing the flow and will generate partial results.
// log - The logger.
func buildYarnV1DependencyMap(packageInfo *PackageInfo, responseStr string, allowPartialResults bool, log utils.Log) (dependenciesMap map[string]*YarnDependency, root *YarnDependency, err error) {
dependenciesMap = make(map[string]*YarnDependency)
var depTree Yarn1Data
err = json.Unmarshal([]byte(responseStr), &depTree)
Expand All @@ -164,23 +175,26 @@ func buildYarnV1DependencyMap(packageInfo *PackageInfo, responseStr string) (dep
packNameToFullName := make(map[string]string)

// Initializing dependencies map without child dependencies for each dependency + creating a map that maps: package-name -> package-name@version
// The two phases mapping is performed since the responseStr from 'yarn list' contains the resolved versions of the dependencies at the map's first level, but may contain the caret version range (^) at the children level.
// Therefore, a manual matching must be made in order to output a map with parent-child relation containing only resolved versions.
for _, curDependency := range depTree.Data.DepTree {
var packageCleanName, packageVersion string
packageCleanName, packageVersion, err = splitNameAndVersion(curDependency.Name)
if err != nil {
return
}

// We insert to dependenciesMap dependencies with the resolved versions only. All dependencies at the responseStr first level contain resolved versions only (their children may contain caret version ranges).
dependenciesMap[curDependency.Name] = &YarnDependency{
Value: curDependency.Name,
Details: YarnDepDetails{Version: packageVersion},
}
packNameToFullName[packageCleanName] = curDependency.Name
}
log.Debug(fmt.Sprintf("'yarn list' output string: %s\n\nPackage name to full name map content: %v", responseStr, packNameToFullName))

// Adding child dependencies for each dependency
for _, curDependency := range depTree.Data.DepTree {
dependency := dependenciesMap[curDependency.Name]
dependencyToUpdateInMap := dependenciesMap[curDependency.Name]

for _, subDep := range curDependency.Dependencies {
var subDepName string
Expand All @@ -191,12 +205,17 @@ func buildYarnV1DependencyMap(packageInfo *PackageInfo, responseStr string) (dep

packageWithResolvedVersion := packNameToFullName[subDepName]
if packageWithResolvedVersion == "" {
if allowPartialResults {
log.Warn(fmt.Sprintf("error occurred during Yarn dependencies map calculation: couldn't find resolved version for '%s' in 'yarn list' output\nFinal rasults may be partial", subDep.DependencyName))
continue
}

err = fmt.Errorf("couldn't find resolved version for '%s' in 'yarn list' output", subDep.DependencyName)
return
}
dependency.Details.Dependencies = append(dependency.Details.Dependencies, YarnDependencyPointer{subDep.DependencyName, packageWithResolvedVersion})
dependencyToUpdateInMap.Details.Dependencies = append(dependencyToUpdateInMap.Details.Dependencies, YarnDependencyPointer{subDep.DependencyName, packageWithResolvedVersion})
}
dependenciesMap[curDependency.Name] = dependency
dependenciesMap[curDependency.Name] = dependencyToUpdateInMap
}

rootDependency := buildYarn1Root(packageInfo, packNameToFullName)
Expand All @@ -205,7 +224,7 @@ func buildYarnV1DependencyMap(packageInfo *PackageInfo, responseStr string) (dep
return
}

// buildYarnV2DependencyMap builds a map of dependencies for Yarn version >= 2.0.0
// Builds a map of dependencies for Yarn version >= 2.0.0
// Note that in some versions of Yarn, the version of the root package is '0.0.0-use.local', instead of the version in the package.json file.
func buildYarnV2DependencyMap(packageInfo *PackageInfo, responseStr string) (dependenciesMap map[string]*YarnDependency, root *YarnDependency, err error) {
dependenciesMap = make(map[string]*YarnDependency)
Expand Down Expand Up @@ -233,7 +252,7 @@ func buildYarnV2DependencyMap(packageInfo *PackageInfo, responseStr string) (dep
return
}

// runYarnInfoOrList depends on the yarn version currently operating on the project, runs the command that gets the dependencies of the project
// Depending on the Yarn version currently in use for the project, this function runs the command that retrieves the project's dependencies
func runYarnInfoOrList(executablePath string, srcPath string, v2AndAbove bool) (outResult, errResult string, err error) {
var command *exec.Cmd
if v2AndAbove {
Expand Down
94 changes: 93 additions & 1 deletion build/utils/yarn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/jfrog/build-info-go/utils"
"github.com/stretchr/testify/assert"
"path/filepath"
"sort"
"strings"
"testing"
)
Expand Down Expand Up @@ -53,7 +54,7 @@ func checkGetYarnDependencies(t *testing.T, versionDir string, expectedLocators
Dependencies: map[string]string{"react": "18.2.0", "xml": "1.0.1"},
DevDependencies: map[string]string{"json": "9.0.6"},
}
dependenciesMap, root, err := GetYarnDependencies(executablePath, projectSrcPath, &pacInfo, &utils.NullLog{})
dependenciesMap, root, err := GetYarnDependencies(executablePath, projectSrcPath, &pacInfo, &utils.NullLog{}, false)
assert.NoError(t, err)
assert.NotNil(t, root)

Expand Down Expand Up @@ -104,6 +105,97 @@ func checkGetYarnDependencies(t *testing.T, versionDir string, expectedLocators
}
}

// This test checks the error handling of buildYarnV1DependencyMap with a response string that is missing a dependency, when allow-partial-results is set to true.
// responseStr, which is an output of 'yarn list' should contain every dependency (direct and indirect) of the project at the first level, with the direct children of each dependency.
// Sometimes the first level is lacking a dependency that appears as a child, or the child dependency is not found at the first level of the map, hence an error should be thrown.
// When apply-partial-results is set to true we expect to provide a partial map instead of dropping the entire flow and return an error in such a case.
func TestBuildYarnV1DependencyMapWithLackingDependencyInResponseString(t *testing.T) {
packageInfo := &PackageInfo{
Name: "test-project",
Version: "1.0.0",
Dependencies: map[string]string{"minimist": "1.2.5", "yarn-inner": "file:./yarn-inner"},
}

// This responseStr simulates should trigger an error since it is missing 'tough-cookie' at the "trees" first level, but this dependency appears as a child for another dependency (and hence should have been in the "trees" level as well)
responseStr := "{\"type\":\"tree\",\"data\":{\"type\":\"list\",\"trees\":[{\"name\":\"[email protected]\",\"children\":[],\"hint\":null,\"color\":\"bold\",\"depth\":0},{\"name\":\"[email protected]\",\"children\":[{\"name\":\"[email protected]\",\"color\":\"dim\",\"shadow\":true}],\"hint\":null,\"color\":\"bold\",\"depth\":0}]}}"

expectedRoot := YarnDependency{
Value: "test-project",
Details: YarnDepDetails{
Version: "1.0.0",
Dependencies: []YarnDependencyPointer{
{
Descriptor: "",
Locator: "[email protected]",
},
{
Descriptor: "",
Locator: "[email protected]",
},
},
},
}

expectedDependenciesMap := map[string]*YarnDependency{
"[email protected]": {
Value: "[email protected]",
Details: YarnDepDetails{
Version: "1.2.5",
Dependencies: nil,
},
},
"[email protected]": {
Value: "[email protected]",
Details: YarnDepDetails{
Version: "1.0.0",
Dependencies: nil,
},
},
"test-project": {
Value: "test-project",
Details: YarnDepDetails{
Version: "1.0.0",
Dependencies: []YarnDependencyPointer{
{
Descriptor: "",
Locator: "[email protected]",
},
{
Descriptor: "",
Locator: "[email protected]",
},
},
},
},
}

dependenciesMap, root, err := buildYarnV1DependencyMap(packageInfo, responseStr, true, &utils.NullLog{})
assert.NoError(t, err)
// Verifying root
assert.NotNil(t, root)
assert.Equal(t, expectedRoot.Value, root.Value)
assert.Len(t, root.Details.Dependencies, len(expectedRoot.Details.Dependencies))
sort.Slice(root.Details.Dependencies, func(i, j int) bool {
return root.Details.Dependencies[i].Locator < root.Details.Dependencies[j].Locator
})
assert.EqualValues(t, expectedRoot.Details.Dependencies, root.Details.Dependencies)

// Verifying dependencies map
assert.Equal(t, len(expectedDependenciesMap), len(dependenciesMap))
for expectedKey, expectedValue := range expectedDependenciesMap {
value := dependenciesMap[expectedKey]
assert.NotNil(t, value)
assert.EqualValues(t, expectedValue.Value, value.Value)
assert.EqualValues(t, expectedValue.Details.Version, value.Details.Version)
if expectedValue.Details.Dependencies != nil {
sort.Slice(value.Details.Dependencies, func(i, j int) bool {
return value.Details.Dependencies[i].Locator < value.Details.Dependencies[j].Locator
})
assert.EqualValues(t, expectedValue.Details.Dependencies, value.Details.Dependencies)
}
}
}

func TestYarnDependency_Name(t *testing.T) {
testCases := []struct {
packageFullName string
Expand Down
2 changes: 1 addition & 1 deletion build/yarn.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (ym *YarnModule) Build() error {
}

func (ym *YarnModule) getDependenciesMap() (map[string]*entities.Dependency, error) {
dependenciesMap, root, err := buildutils.GetYarnDependencies(ym.executablePath, ym.srcPath, ym.packageInfo, ym.containingBuild.logger)
dependenciesMap, root, err := buildutils.GetYarnDependencies(ym.executablePath, ym.srcPath, ym.packageInfo, ym.containingBuild.logger, false)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit e7739ae

Please sign in to comment.