github.com/jfrog/jfrog-cli-core@v1.12.1/artifactory/commands/npm/installorci.go (about)

     1  package npm
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	commandUtils "github.com/jfrog/jfrog-cli-core/artifactory/commands/utils"
     8  	npmutils "github.com/jfrog/jfrog-cli-core/utils/npm"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/buger/jsonparser"
    16  	gofrogcmd "github.com/jfrog/gofrog/io"
    17  	"github.com/jfrog/gofrog/parallel"
    18  	"github.com/jfrog/jfrog-cli-core/artifactory/utils"
    19  	"github.com/jfrog/jfrog-cli-core/artifactory/utils/npm"
    20  	"github.com/jfrog/jfrog-cli-core/utils/config"
    21  	"github.com/jfrog/jfrog-client-go/artifactory"
    22  	"github.com/jfrog/jfrog-client-go/artifactory/buildinfo"
    23  	"github.com/jfrog/jfrog-client-go/auth"
    24  	clientutils "github.com/jfrog/jfrog-client-go/utils"
    25  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    26  	"github.com/jfrog/jfrog-client-go/utils/log"
    27  	"github.com/jfrog/jfrog-client-go/utils/version"
    28  )
    29  
    30  const npmrcFileName = ".npmrc"
    31  const npmrcBackupFileName = "jfrog.npmrc.backup"
    32  const minSupportedNpmVersion = "5.4.0"
    33  
    34  type NpmCommandArgs struct {
    35  	command          string
    36  	threads          int
    37  	jsonOutput       bool
    38  	executablePath   string
    39  	restoreNpmrcFunc func() error
    40  	workingDirectory string
    41  	registry         string
    42  	npmAuth          string
    43  	collectBuildInfo bool
    44  	dependencies     map[string]*dependency
    45  	typeRestriction  typeRestriction
    46  	authArtDetails   auth.ServiceDetails
    47  	packageInfo      *npmutils.PackageInfo
    48  	npmVersion       *version.Version
    49  	NpmCommand
    50  }
    51  
    52  type typeRestriction int
    53  
    54  const (
    55  	defaultRestriction typeRestriction = iota
    56  	all
    57  	devOnly
    58  	prodOnly
    59  )
    60  
    61  type NpmInstallOrCiCommand struct {
    62  	configFilePath      string
    63  	internalCommandName string
    64  	*NpmCommandArgs
    65  }
    66  
    67  func NewNpmInstallCommand() *NpmInstallOrCiCommand {
    68  	return &NpmInstallOrCiCommand{NpmCommandArgs: NewNpmCommandArgs("install"), internalCommandName: "rt_npm_install"}
    69  }
    70  
    71  func NewNpmCiCommand() *NpmInstallOrCiCommand {
    72  	return &NpmInstallOrCiCommand{NpmCommandArgs: NewNpmCommandArgs("ci"), internalCommandName: "rt_npm_ci"}
    73  }
    74  
    75  func (nic *NpmInstallOrCiCommand) CommandName() string {
    76  	return nic.internalCommandName
    77  }
    78  
    79  func (nic *NpmInstallOrCiCommand) SetConfigFilePath(configFilePath string) *NpmInstallOrCiCommand {
    80  	nic.configFilePath = configFilePath
    81  	return nic
    82  }
    83  
    84  func (nic *NpmInstallOrCiCommand) SetArgs(args []string) *NpmInstallOrCiCommand {
    85  	nic.NpmCommandArgs.npmArgs = args
    86  	return nic
    87  }
    88  
    89  func (nic *NpmInstallOrCiCommand) SetRepoConfig(conf *utils.RepositoryConfig) *NpmInstallOrCiCommand {
    90  	serverDetails, _ := conf.ServerDetails()
    91  	nic.NpmCommandArgs.SetRepo(conf.TargetRepo()).SetServerDetails(serverDetails)
    92  	return nic
    93  }
    94  
    95  func (nic *NpmInstallOrCiCommand) Run() error {
    96  	log.Info(fmt.Sprintf("Running npm %s.", nic.command))
    97  	// Read config file.
    98  	log.Debug("Preparing to read the config file", nic.configFilePath)
    99  	vConfig, err := utils.ReadConfigFile(nic.configFilePath, utils.YAML)
   100  	if err != nil {
   101  		return err
   102  	}
   103  	// Extract resolution params.
   104  	resolverParams, err := utils.GetRepoConfigByPrefix(nic.configFilePath, utils.ProjectConfigResolverPrefix, vConfig)
   105  	if err != nil {
   106  		return err
   107  	}
   108  	threads, _, filteredNpmArgs, buildConfiguration, err := commandUtils.ExtractNpmOptionsFromArgs(nic.npmArgs)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	nic.SetRepoConfig(resolverParams).SetArgs(filteredNpmArgs).SetThreads(threads).SetBuildConfiguration(buildConfiguration)
   113  	return nic.run()
   114  }
   115  
   116  func (nca *NpmCommandArgs) SetThreads(threads int) *NpmCommandArgs {
   117  	nca.threads = threads
   118  	return nca
   119  }
   120  
   121  func NewNpmCommandArgs(npmCommand string) *NpmCommandArgs {
   122  	return &NpmCommandArgs{command: npmCommand}
   123  }
   124  
   125  func (nca *NpmCommandArgs) ServerDetails() (*config.ServerDetails, error) {
   126  	return nca.serverDetails, nil
   127  }
   128  
   129  func (nca *NpmCommandArgs) run() error {
   130  	if err := nca.preparePrerequisites(nca.repo); err != nil {
   131  		return err
   132  	}
   133  
   134  	if err := nca.createTempNpmrc(); err != nil {
   135  		return nca.restoreNpmrcAndError(err)
   136  	}
   137  
   138  	if err := nca.runInstallOrCi(); err != nil {
   139  		return nca.restoreNpmrcAndError(err)
   140  	}
   141  
   142  	if err := nca.restoreNpmrcFunc(); err != nil {
   143  		return err
   144  	}
   145  
   146  	if !nca.collectBuildInfo {
   147  		log.Info(fmt.Sprintf("npm %s finished successfully.", nca.command))
   148  		return nil
   149  	}
   150  
   151  	if err := nca.setDependenciesList(); err != nil {
   152  		return err
   153  	}
   154  
   155  	if err := nca.collectDependenciesChecksums(); err != nil {
   156  		return err
   157  	}
   158  
   159  	if err := nca.saveDependenciesData(); err != nil {
   160  		return err
   161  	}
   162  
   163  	log.Info(fmt.Sprintf("npm %s finished successfully.", nca.command))
   164  	return nil
   165  }
   166  
   167  func (nca *NpmCommandArgs) preparePrerequisites(repo string) error {
   168  	log.Debug("Preparing prerequisites.")
   169  	var err error
   170  	if err = nca.setNpmExecutable(); err != nil {
   171  		return err
   172  	}
   173  
   174  	if err = nca.validateNpmVersion(); err != nil {
   175  		return err
   176  	}
   177  
   178  	if err := nca.setJsonOutput(); err != nil {
   179  		return err
   180  	}
   181  
   182  	nca.workingDirectory, err = commandUtils.GetWorkingDirectory()
   183  	if err != nil {
   184  		return err
   185  	}
   186  	log.Debug("Working directory set to:", nca.workingDirectory)
   187  
   188  	if err = nca.setArtifactoryAuth(); err != nil {
   189  		return err
   190  	}
   191  
   192  	nca.npmAuth, nca.registry, err = commandUtils.GetArtifactoryNpmRepoDetails(repo, &nca.authArtDetails)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	nca.collectBuildInfo, nca.packageInfo, err = commandUtils.PrepareBuildInfo(nca.workingDirectory, nca.buildConfiguration, nca.npmVersion)
   198  	if err != nil {
   199  		return err
   200  	}
   201  
   202  	nca.restoreNpmrcFunc, err = commandUtils.BackupFile(filepath.Join(nca.workingDirectory, npmrcFileName), filepath.Join(nca.workingDirectory, npmrcBackupFileName))
   203  	return err
   204  }
   205  
   206  func (nca *NpmCommandArgs) setJsonOutput() error {
   207  	jsonOutput, err := npm.ConfigGet(nca.npmArgs, "json", nca.executablePath)
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	// In case of --json=<not boolean>, the value of json is set to 'true', but the result from the command is not 'true'
   213  	nca.jsonOutput = jsonOutput != "false"
   214  	return nil
   215  }
   216  
   217  func createRestoreErrorPrefix(workingDirectory string) string {
   218  	return fmt.Sprintf("Error occurred while restoring project .npmrc file. "+
   219  		"Delete '%s' and move '%s' (if exists) to '%s' in order to restore the project. Failure cause: \n",
   220  		filepath.Join(workingDirectory, npmrcFileName),
   221  		filepath.Join(workingDirectory, npmrcBackupFileName),
   222  		filepath.Join(workingDirectory, npmrcFileName))
   223  }
   224  
   225  // In order to make sure the install/ci downloads the artifacts from Artifactory we create a .npmrc file in the project dir.
   226  // If such a file exists we back it up as npmrcBackupFileName.
   227  func (nca *NpmCommandArgs) createTempNpmrc() error {
   228  	log.Debug("Creating project .npmrc file.")
   229  	data, err := npm.GetConfigList(nca.npmArgs, nca.executablePath)
   230  	configData, err := nca.prepareConfigData(data)
   231  	if err != nil {
   232  		return errorutils.CheckError(err)
   233  	}
   234  
   235  	if err = removeNpmrcIfExists(nca.workingDirectory); err != nil {
   236  		return err
   237  	}
   238  
   239  	return errorutils.CheckError(os.WriteFile(filepath.Join(nca.workingDirectory, npmrcFileName), configData, 0600))
   240  }
   241  
   242  func (nca *NpmCommandArgs) runInstallOrCi() error {
   243  	log.Debug(fmt.Sprintf("Running npm %s command.", nca.command))
   244  	filteredArgs := filterFlags(nca.npmArgs)
   245  	npmCmdConfig := &npm.NpmConfig{
   246  		Npm:          nca.executablePath,
   247  		Command:      append([]string{nca.command}, filteredArgs...),
   248  		CommandFlags: nil,
   249  		StrWriter:    nil,
   250  		ErrWriter:    nil,
   251  	}
   252  
   253  	if nca.collectBuildInfo && len(filteredArgs) > 0 {
   254  		log.Warn("Build info dependencies collection with npm arguments is not supported. Build info creation will be skipped.")
   255  		nca.collectBuildInfo = false
   256  	}
   257  
   258  	return errorutils.CheckError(gofrogcmd.RunCmd(npmCmdConfig))
   259  }
   260  
   261  func (nca *NpmCommandArgs) setDependenciesList() (err error) {
   262  	nca.dependencies = make(map[string]*dependency)
   263  	// nca.typeRestriction default is 'all'
   264  	if nca.typeRestriction != prodOnly {
   265  		if err = nca.prepareDependencies("dev"); err != nil {
   266  			return
   267  		}
   268  	}
   269  	if nca.typeRestriction != devOnly {
   270  		err = nca.prepareDependencies("prod")
   271  	}
   272  	return
   273  }
   274  
   275  func (nca *NpmCommandArgs) collectDependenciesChecksums() error {
   276  	log.Info("Collecting dependencies information... For the first run of the build, this may take a few minutes. Subsequent runs should be faster.")
   277  	servicesManager, err := utils.CreateServiceManager(nca.serverDetails, -1, false)
   278  	if err != nil {
   279  		return err
   280  	}
   281  
   282  	previousBuildDependencies, err := commandUtils.GetDependenciesFromLatestBuild(servicesManager, nca.buildConfiguration.BuildName)
   283  	if err != nil {
   284  		return err
   285  	}
   286  	producerConsumer := parallel.NewBounedRunner(nca.threads, false)
   287  	errorsQueue := clientutils.NewErrorsQueue(1)
   288  	handlerFunc := nca.createGetDependencyInfoFunc(servicesManager, previousBuildDependencies)
   289  	go func() {
   290  		defer producerConsumer.Done()
   291  		for i := range nca.dependencies {
   292  			producerConsumer.AddTaskWithError(handlerFunc(i), errorsQueue.AddError)
   293  		}
   294  	}()
   295  	producerConsumer.Run()
   296  	return errorsQueue.GetError()
   297  }
   298  
   299  func (nca *NpmCommandArgs) saveDependenciesData() error {
   300  	log.Debug("Saving data.")
   301  	if nca.buildConfiguration.Module == "" {
   302  		nca.buildConfiguration.Module = nca.packageInfo.BuildInfoModuleId()
   303  	}
   304  
   305  	dependencies, missingDependencies := nca.transformDependencies()
   306  	if err := commandUtils.SaveDependenciesData(dependencies, nca.buildConfiguration); err != nil {
   307  		return err
   308  	}
   309  
   310  	commandUtils.PrintMissingDependencies(missingDependencies)
   311  	return nil
   312  }
   313  
   314  func (nca *NpmCommandArgs) validateNpmVersion() error {
   315  	npmVersion, err := npmutils.Version(nca.executablePath)
   316  	if err != nil {
   317  		return err
   318  	}
   319  	if npmVersion.Compare(minSupportedNpmVersion) > 0 {
   320  		return errorutils.CheckError(errors.New(fmt.Sprintf(
   321  			"JFrog CLI npm %s command requires npm client version "+minSupportedNpmVersion+" or higher", nca.command)))
   322  	}
   323  	nca.npmVersion = npmVersion
   324  	return nil
   325  }
   326  
   327  // This func transforms "npm config list" result to key=val list of values that can be set to .npmrc file.
   328  // it filters any nil values key, changes registry and scope registries to Artifactory url and adds Artifactory authentication to the list
   329  func (nca *NpmCommandArgs) prepareConfigData(data []byte) ([]byte, error) {
   330  	var filteredConf []string
   331  	configString := string(data)
   332  	scanner := bufio.NewScanner(strings.NewReader(configString))
   333  
   334  	for scanner.Scan() {
   335  		currOption := scanner.Text()
   336  		if currOption != "" {
   337  			splitOption := strings.SplitN(currOption, "=", 2)
   338  			key := strings.TrimSpace(splitOption[0])
   339  			if len(splitOption) == 2 && isValidKey(key) {
   340  				value := strings.TrimSpace(splitOption[1])
   341  				if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
   342  					filteredConf = addArrayConfigs(filteredConf, key, value)
   343  				} else {
   344  					filteredConf = append(filteredConf, currOption, "\n")
   345  				}
   346  				nca.setTypeRestriction(key, value)
   347  			} else if strings.HasPrefix(splitOption[0], "@") {
   348  				// Override scoped registries (@scope = xyz)
   349  				filteredConf = append(filteredConf, splitOption[0], " = ", nca.registry, "\n")
   350  			}
   351  		}
   352  	}
   353  	if err := scanner.Err(); err != nil {
   354  		return nil, errorutils.CheckError(err)
   355  	}
   356  
   357  	filteredConf = append(filteredConf, "json = ", strconv.FormatBool(nca.jsonOutput), "\n")
   358  	filteredConf = append(filteredConf, "registry = ", nca.registry, "\n")
   359  	filteredConf = append(filteredConf, nca.npmAuth)
   360  	return []byte(strings.Join(filteredConf, "")), nil
   361  }
   362  
   363  // Gets a config with value which is an array, and adds it to the conf list
   364  func addArrayConfigs(conf []string, key, arrayValue string) []string {
   365  	if arrayValue == "[]" {
   366  		return conf
   367  	}
   368  
   369  	values := strings.TrimPrefix(strings.TrimSuffix(arrayValue, "]"), "[")
   370  	valuesSlice := strings.Split(values, ",")
   371  	for _, val := range valuesSlice {
   372  		confToAdd := fmt.Sprintf("%s[] = %s", key, val)
   373  		conf = append(conf, confToAdd, "\n")
   374  	}
   375  
   376  	return conf
   377  }
   378  
   379  func (nca *NpmCommandArgs) setTypeRestriction(key string, value string) {
   380  	// From npm 7, type restriction is determined by 'omit' and 'include' (both appear in 'npm config ls').
   381  	// Other options (like 'dev', 'production' and 'only') are deprecated, but if they're used anyway - 'omit' and 'include' are automatically calculated.
   382  	// So 'omit' is always preferred, if it exists.
   383  	if key == "omit" {
   384  		if strings.Contains(value, "dev") {
   385  			nca.typeRestriction = prodOnly
   386  		} else {
   387  			nca.typeRestriction = all
   388  		}
   389  	} else if nca.typeRestriction == defaultRestriction { // Until npm 6, configurations in 'npm config ls' are sorted by priority in descending order, so typeRestriction should be set only if it was not set before
   390  		if key == "only" {
   391  			if strings.Contains(value, "prod") {
   392  				nca.typeRestriction = prodOnly
   393  			} else if strings.Contains(value, "dev") {
   394  				nca.typeRestriction = devOnly
   395  			}
   396  		} else if key == "production" && strings.Contains(value, "true") {
   397  			nca.typeRestriction = prodOnly
   398  		}
   399  	}
   400  }
   401  
   402  // Run npm list and parse the returned JSON.
   403  // typeRestriction must be one of: 'dev' or 'prod'!
   404  func (nca *NpmCommandArgs) prepareDependencies(typeRestriction string) error {
   405  	// Run npm list
   406  	// Although this command can get --development as a flag (according to npm docs), it's not working on npm 6.
   407  	// Although this command can get --only=development as a flag (according to npm docs), it's not working on npm 7.
   408  	data, errData, err := npm.RunList(strings.Join(append(nca.npmArgs, "--all", "--"+typeRestriction), " "), nca.executablePath)
   409  	if err != nil {
   410  		log.Warn("npm list command failed with error:", err.Error())
   411  	}
   412  	if len(errData) > 0 {
   413  		log.Warn("Some errors occurred while collecting dependencies info:\n" + string(errData))
   414  	}
   415  
   416  	// Parse the dependencies json object
   417  	return jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) (err error) {
   418  		if string(key) == "dependencies" {
   419  			err = nca.parseDependencies(value, typeRestriction, []string{nca.packageInfo.BuildInfoModuleId()})
   420  		}
   421  		return err
   422  	})
   423  }
   424  
   425  // Parses npm dependencies recursively and adds the collected dependencies to nca.dependencies
   426  func (nca *NpmCommandArgs) parseDependencies(data []byte, scope string, pathToRoot []string) error {
   427  	return jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
   428  		depName := string(key)
   429  		ver, _, _, err := jsonparser.Get(data, depName, "version")
   430  		depVersion := string(ver)
   431  		depKey := depName + ":" + depVersion
   432  		if err != nil && err != jsonparser.KeyPathNotFoundError {
   433  			return errorutils.CheckError(err)
   434  		} else if err == jsonparser.KeyPathNotFoundError {
   435  			log.Debug(fmt.Sprintf("%s dependency will not be included in the build-info, because the 'npm ls' command did not return its version.\nThe reason why the version wasn't returned may be because the package is a 'peerdependency', which was not manually installed.\n'npm install' does not download 'peerdependencies' automatically. It is therefore okay to skip this dependency.", depName))
   436  		} else {
   437  			nca.appendDependency(depKey, depName, depVersion, scope, pathToRoot)
   438  		}
   439  		transitive, _, _, err := jsonparser.Get(data, depName, "dependencies")
   440  		if err != nil && err.Error() != "Key path not found" {
   441  			return errorutils.CheckError(err)
   442  		}
   443  		if len(transitive) > 0 {
   444  			if err := nca.parseDependencies(transitive, scope, append([]string{depKey}, pathToRoot...)); err != nil {
   445  				return err
   446  			}
   447  		}
   448  		return nil
   449  	})
   450  }
   451  
   452  func (nca *NpmCommandArgs) appendDependency(depKey, depName, depVersion, scope string, pathToRoot []string) {
   453  	if nca.dependencies[depKey] == nil {
   454  		nca.dependencies[depKey] = &dependency{name: depName, version: depVersion, scopes: []string{scope}}
   455  	} else if !scopeAlreadyExists(scope, nca.dependencies[depKey].scopes) {
   456  		nca.dependencies[depKey].scopes = append(nca.dependencies[depKey].scopes, scope)
   457  	}
   458  	nca.dependencies[depKey].pathToRoot = append(nca.dependencies[depKey].pathToRoot, pathToRoot)
   459  }
   460  
   461  // Creates a function that fetches dependency data.
   462  // If a dependency was included in the previous build, take the checksums information from it.
   463  // Otherwise, fetch the checksum from Artifactory.
   464  // Can be applied from a producer-consumer mechanism.
   465  func (nca *NpmCommandArgs) createGetDependencyInfoFunc(servicesManager artifactory.ArtifactoryServicesManager,
   466  	previousBuildDependencies map[string]*buildinfo.Dependency) getDependencyInfoFunc {
   467  	return func(dependencyIndex string) parallel.TaskFunc {
   468  		return func(threadId int) error {
   469  			name := nca.dependencies[dependencyIndex].name
   470  			ver := nca.dependencies[dependencyIndex].version
   471  
   472  			// Get dependency info.
   473  			checksum, fileType, err := commandUtils.GetDependencyInfo(name, ver, previousBuildDependencies, servicesManager, threadId)
   474  			if err != nil || checksum.IsEmpty() {
   475  				return err
   476  			}
   477  
   478  			// Update dependency.
   479  			nca.dependencies[dependencyIndex].fileType = fileType
   480  			nca.dependencies[dependencyIndex].checksum = checksum
   481  			return nil
   482  		}
   483  	}
   484  }
   485  
   486  // Transforms the list of dependencies to buildinfo.Dependencies list and creates a list of dependencies that are missing in Artifactory.
   487  func (nca *NpmCommandArgs) transformDependencies() (dependencies []buildinfo.Dependency, missingDependencies []buildinfo.Dependency) {
   488  	for _, dependency := range nca.dependencies {
   489  		biDependency := buildinfo.Dependency{Id: dependency.name + ":" + dependency.version, Type: dependency.fileType,
   490  			Scopes: dependency.scopes, Checksum: dependency.checksum, RequestedBy: dependency.pathToRoot}
   491  		if !dependency.checksum.IsEmpty() {
   492  			dependencies = append(dependencies,
   493  				biDependency)
   494  		} else {
   495  			missingDependencies = append(missingDependencies, biDependency)
   496  		}
   497  	}
   498  	return
   499  }
   500  
   501  func (nca *NpmCommandArgs) restoreNpmrcAndError(err error) error {
   502  	if restoreErr := nca.restoreNpmrcFunc(); restoreErr != nil {
   503  		return errorutils.CheckError(errors.New(fmt.Sprintf("Two errors occurred:\n %s\n %s", restoreErr.Error(), err.Error())))
   504  	}
   505  	return err
   506  }
   507  
   508  func (nca *NpmCommandArgs) setArtifactoryAuth() error {
   509  	authArtDetails, err := nca.serverDetails.CreateArtAuthConfig()
   510  	if err != nil {
   511  		return err
   512  	}
   513  	if authArtDetails.GetSshAuthHeaders() != nil {
   514  		return errorutils.CheckError(errors.New("SSH authentication is not supported in this command"))
   515  	}
   516  	nca.authArtDetails = authArtDetails
   517  	return nil
   518  }
   519  
   520  func removeNpmrcIfExists(workingDirectory string) error {
   521  	if _, err := os.Stat(filepath.Join(workingDirectory, npmrcFileName)); err != nil {
   522  		if os.IsNotExist(err) { // The file dose not exist, nothing to do.
   523  			return nil
   524  		}
   525  		return errorutils.CheckError(err)
   526  	}
   527  
   528  	log.Debug("Removing Existing .npmrc file")
   529  	return errorutils.CheckError(os.Remove(filepath.Join(workingDirectory, npmrcFileName)))
   530  }
   531  
   532  func (nca *NpmCommandArgs) setNpmExecutable() error {
   533  	npmExecPath, err := exec.LookPath("npm")
   534  	if err != nil {
   535  		return errorutils.CheckError(err)
   536  	}
   537  
   538  	if npmExecPath == "" {
   539  		return errorutils.CheckError(errors.New("could not find 'npm' executable"))
   540  	}
   541  	nca.executablePath = npmExecPath
   542  	log.Debug("Found npm executable at:", nca.executablePath)
   543  	return nil
   544  }
   545  
   546  func scopeAlreadyExists(scope string, existingScopes []string) bool {
   547  	for _, existingScope := range existingScopes {
   548  		if existingScope == scope {
   549  			return true
   550  		}
   551  	}
   552  	return false
   553  }
   554  
   555  // To avoid writing configurations that are used by us
   556  func isValidKey(key string) bool {
   557  	return !strings.HasPrefix(key, "//") &&
   558  		!strings.HasPrefix(key, ";") && // Comments
   559  		!strings.HasPrefix(key, "@") && // Scoped configurations
   560  		key != "registry" &&
   561  		key != "metrics-registry" &&
   562  		key != "json" // Handled separately because 'npm c ls' should run with json=false
   563  }
   564  
   565  func filterFlags(splitArgs []string) []string {
   566  	var filteredArgs []string
   567  	for _, arg := range splitArgs {
   568  		if !strings.HasPrefix(arg, "-") {
   569  			filteredArgs = append(filteredArgs, arg)
   570  		}
   571  	}
   572  	return filteredArgs
   573  }
   574  
   575  type getDependencyInfoFunc func(string) parallel.TaskFunc
   576  
   577  type dependency struct {
   578  	name       string
   579  	version    string
   580  	scopes     []string
   581  	fileType   string
   582  	checksum   buildinfo.Checksum
   583  	pathToRoot [][]string
   584  }