diff --git a/build/utils/npm.go b/build/utils/npm.go index d086cc11..b962f8c9 100644 --- a/build/utils/npm.go +++ b/build/utils/npm.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "golang.org/x/exp/slices" "os" "os/exec" "path/filepath" @@ -16,6 +17,8 @@ import ( "github.com/jfrog/gofrog/version" ) +const npmInstallCommand = "install" + // CalculateNpmDependenciesList gets an npm project's dependencies. func CalculateNpmDependenciesList(executablePath, srcPath, moduleId string, npmParams NpmTreeDepListParam, calculateChecksums bool, log utils.Log) ([]entities.Dependency, error) { if log == nil { @@ -136,7 +139,7 @@ func runNpmLsWithoutNodeModules(executablePath, srcPath string, npmListParams Np return nil, isDirExistsErr } if !isPackageLockExist || (npmListParams.OverwritePackageLock && checkIfLockFileShouldBeUpdated(srcPath, log)) { - err := installPackageLock(executablePath, srcPath, npmListParams.Args, log, npmVersion) + err := installPackageLock(executablePath, srcPath, npmListParams.InstallCommandArgs, npmListParams.Args, log, npmVersion) if err != nil { return nil, err } @@ -151,9 +154,11 @@ func runNpmLsWithoutNodeModules(executablePath, srcPath string, npmListParams Np return data, nil } -func installPackageLock(executablePath, srcPath string, npmArgs []string, log utils.Log, npmVersion *version.Version) error { +func installPackageLock(executablePath, srcPath string, npmInstallCommandArgs, npmArgs []string, log utils.Log, npmVersion *version.Version) error { if npmVersion.AtLeast("6.0.0") { npmArgs = append(npmArgs, "--package-lock-only") + // Including any 'install' command flags that were supplied by the user in preceding steps of the process, while ensuring that duplicates are avoided. + npmArgs = append(npmArgs, filterUniqueArgs(npmInstallCommandArgs, npmArgs)...) // Installing package-lock to generate the dependencies map. _, _, err := RunNpmCmd(executablePath, srcPath, AppendNpmCommand(npmArgs, "install"), log) if err != nil { @@ -164,6 +169,20 @@ func installPackageLock(executablePath, srcPath string, npmArgs []string, log ut return errors.New("it looks like you’re using version " + npmVersion.GetVersion() + " of the npm client. Versions below 6.0.0 require running `npm install` before running this command") } +// Removes any arguments from argsToFilter that are already present in existingArgs. Furthermore, excludes the "install" command and retains only the flags in the resulting argument list. +func filterUniqueArgs(argsToFilter []string, existingArgs []string) []string { + var filteredArgs []string + for _, arg := range argsToFilter { + if arg == npmInstallCommand { + continue + } + if !slices.Contains(existingArgs, arg) { + filteredArgs = append(filteredArgs, arg) + } + } + return filteredArgs +} + // Check if package.json has been modified. // This might indicate the addition of new packages to package.json that haven't been reflected in package-lock.json. func checkIfLockFileShouldBeUpdated(srcPath string, log utils.Log) bool { @@ -192,7 +211,10 @@ func GetNpmVersion(executablePath string, log utils.Log) (*version.Version, erro } type NpmTreeDepListParam struct { + // Required for the 'install' and 'ls' commands that could be triggered during the construction of the NPM dependency tree Args []string + // Optional user-supplied arguments for the 'install' command. These arguments are not available from all entry points. They may be employed when constructing the NPM dependency tree, which could necessitate the execution of 'npm install...' + InstallCommandArgs []string // Ignore the node_modules folder if exists, using the '--package-lock-only' flag IgnoreNodeModules bool // Rewrite package-lock.json, if exists. diff --git a/build/utils/npm_test.go b/build/utils/npm_test.go index f8363c6e..7ef9ad67 100644 --- a/build/utils/npm_test.go +++ b/build/utils/npm_test.go @@ -25,7 +25,7 @@ func TestReadPackageInfo(t *testing.T) { return } - tests := []struct { + testcases := []struct { json string pi *PackageInfo }{ @@ -35,7 +35,7 @@ func TestReadPackageInfo(t *testing.T) { &PackageInfo{Name: "build-info-go-tests", Version: "1.0.0", Scope: "@jfrog"}}, {`{}`, &PackageInfo{}}, } - for _, test := range tests { + for _, test := range testcases { t.Run(test.json, func(t *testing.T) { packInfo, err := ReadPackageInfo([]byte(test.json), npmVersion) assert.NoError(t, err) @@ -94,14 +94,14 @@ func TestReadPackageInfoFromPackageJsonIfExistErr(t *testing.T) { } func TestGetDeployPath(t *testing.T) { - tests := []struct { + testcases := []struct { expectedPath string pi *PackageInfo }{ {`build-info-go-tests/-/build-info-go-tests-1.0.0.tgz`, &PackageInfo{Name: "build-info-go-tests", Version: "1.0.0", Scope: ""}}, {`@jfrog/build-info-go-tests/-/build-info-go-tests-1.0.0.tgz`, &PackageInfo{Name: "build-info-go-tests", Version: "1.0.0", Scope: "@jfrog"}}, } - for _, test := range tests { + for _, test := range testcases { t.Run(test.expectedPath, func(t *testing.T) { assert.Equal(t, test.expectedPath, test.pi.GetDeployPath()) }) @@ -430,3 +430,36 @@ func validateDependencies(t *testing.T, projectPath string, npmArgs []string) { // Asserting there is at least one dependency. assert.Greater(t, len(dependencies), 0, "Error: dependencies are not found!") } + +func TestFilterUniqueArgs(t *testing.T) { + var testcases = []struct { + argsToFilter []string + alreadyExists []string + expectedResult []string + }{ + { + argsToFilter: []string{"install"}, + alreadyExists: []string{}, + expectedResult: nil, + }, + { + argsToFilter: []string{"install", "--flagA"}, + alreadyExists: []string{"--flagA"}, + expectedResult: nil, + }, + { + argsToFilter: []string{"install", "--flagA", "--flagB"}, + alreadyExists: []string{"--flagA"}, + expectedResult: []string{"--flagB"}, + }, + { + argsToFilter: []string{"install", "--flagA", "--flagB"}, + alreadyExists: []string{"--flagA", "--flagC"}, + expectedResult: []string{"--flagB"}, + }, + } + + for _, testcase := range testcases { + assert.Equal(t, testcase.expectedResult, filterUniqueArgs(testcase.argsToFilter, testcase.alreadyExists)) + } +}