package npm import ( "bytes" "encoding/json" "fmt" "github.com/SAP/jenkins-library/pkg/command" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/piperutils" "io" "os" "path/filepath" "strings" ) // Execute struct holds utils to enable mocking and common parameters type Execute struct { Utils Utils Options ExecutorOptions } // Executor interface to enable mocking for testing type Executor interface { FindPackageJSONFiles() []string FindPackageJSONFilesWithScript(packageJSONFiles []string, script string) ([]string, error) RunScriptsInAllPackages(runScripts []string, runOptions []string, virtualFrameBuffer bool) error InstallAllDependencies(packageJSONFiles []string) error SetNpmRegistries() error } // ExecutorOptions holds common parameters for functions of Executor type ExecutorOptions struct { DefaultNpmRegistry string SapNpmRegistry string ExecRunner ExecRunner } // NewExecutor instantiates Execute struct and sets executeOptions func NewExecutor(executorOptions ExecutorOptions) Executor { utils := utilsBundle{Files: &piperutils.Files{}, execRunner: executorOptions.ExecRunner} return &Execute{ Utils: &utils, Options: executorOptions, } } // ExecRunner interface to enable mocking for testing type ExecRunner interface { SetEnv(e []string) Stdout(out io.Writer) Stderr(out io.Writer) RunExecutable(executable string, params ...string) error RunExecutableInBackground(executable string, params ...string) (command.Execution, error) } // Utils interface for mocking type Utils interface { Chdir(path string) error FileExists(filename string) (bool, error) FileRead(path string) ([]byte, error) Getwd() (string, error) Glob(pattern string) (matches []string, err error) GetExecRunner() ExecRunner } type utilsBundle struct { *piperutils.Files execRunner ExecRunner } // GetExecRunner returns an execRunner if it's not yet initialized func (u *utilsBundle) GetExecRunner() ExecRunner { if u.execRunner == nil { u.execRunner = &command.Command{} u.execRunner.Stdout(log.Writer()) u.execRunner.Stderr(log.Writer()) } return u.execRunner } // SetNpmRegistries configures the given npm registries. // CAUTION: This will change the npm configuration in the user's home directory. func (exec *Execute) SetNpmRegistries() error { execRunner := exec.Utils.GetExecRunner() const sapRegistry = "@sap:registry" const npmRegistry = "registry" configurableRegistries := []string{npmRegistry, sapRegistry} for _, registry := range configurableRegistries { var buffer bytes.Buffer execRunner.Stdout(&buffer) err := execRunner.RunExecutable("npm", "config", "get", registry) execRunner.Stdout(log.Writer()) if err != nil { return err } preConfiguredRegistry := buffer.String() if registryIsNonEmpty(preConfiguredRegistry) { log.Entry().Info("Discovered pre-configured npm registry " + registry + " with value " + preConfiguredRegistry) } if registry == npmRegistry && exec.Options.DefaultNpmRegistry != "" && registryRequiresConfiguration(preConfiguredRegistry, "https://registry.npmjs.org") { log.Entry().Info("npm registry " + registry + " was not configured, setting it to " + exec.Options.DefaultNpmRegistry) err = execRunner.RunExecutable("npm", "config", "set", registry, exec.Options.DefaultNpmRegistry) if err != nil { return err } } if registry == sapRegistry && exec.Options.SapNpmRegistry != "" && registryRequiresConfiguration(preConfiguredRegistry, "https://npm.sap.com") { log.Entry().Info("npm registry " + registry + " was not configured, setting it to " + exec.Options.SapNpmRegistry) err = execRunner.RunExecutable("npm", "config", "set", registry, exec.Options.SapNpmRegistry) if err != nil { return err } } } return nil } func registryIsNonEmpty(preConfiguredRegistry string) bool { return !strings.HasPrefix(preConfiguredRegistry, "undefined") && len(preConfiguredRegistry) > 0 } func registryRequiresConfiguration(preConfiguredRegistry, url string) bool { return strings.HasPrefix(preConfiguredRegistry, "undefined") || strings.HasPrefix(preConfiguredRegistry, url) } // RunScriptsInAllPackages runs all scripts defined in ExecuteOptions.RunScripts func (exec *Execute) RunScriptsInAllPackages(runScripts []string, runOptions []string, virtualFrameBuffer bool) error { packageJSONFiles := exec.FindPackageJSONFiles() execRunner := exec.Utils.GetExecRunner() if virtualFrameBuffer { cmd, err := execRunner.RunExecutableInBackground("Xvfb", "-ac", ":99", "-screen", "0", "1280x1024x16") if err != nil { return fmt.Errorf("failed to start virtual frame buffer%w", err) } defer cmd.Kill() execRunner.SetEnv([]string{"DISPLAY=:99"}) } for _, script := range runScripts { packagesWithScript, err := exec.FindPackageJSONFilesWithScript(packageJSONFiles, script) if err != nil { return err } if len(packagesWithScript) == 0 { log.Entry().Warnf("could not find any package.json file with script " + script) continue } for _, packageJSON := range packagesWithScript { err = exec.executeScript(packageJSON, script, runOptions) if err != nil { return err } } } return nil } func (exec *Execute) executeScript(packageJSON string, script string, runOptions []string) error { execRunner := exec.Utils.GetExecRunner() oldWorkingDirectory, err := exec.Utils.Getwd() if err != nil { return fmt.Errorf("failed to get current working directory before executing npm scripts: %w", err) } dir := filepath.Dir(packageJSON) err = exec.Utils.Chdir(dir) if err != nil { return fmt.Errorf("failed to change into directory for executing script: %w", err) } // set in each directory to respect existing config in rc fileUtils err = exec.SetNpmRegistries() if err != nil { return err } log.Entry().WithField("WorkingDirectory", dir).Info("run-script " + script) npmRunArgs := []string{"run", script} if len(runOptions) > 0 { npmRunArgs = append(npmRunArgs, runOptions...) } err = execRunner.RunExecutable("npm", npmRunArgs...) if err != nil { return fmt.Errorf("failed to run npm script %s: %w", script, err) } err = exec.Utils.Chdir(oldWorkingDirectory) if err != nil { return fmt.Errorf("failed to change back into original directory: %w", err) } return nil } // FindPackageJSONFiles returns a list of all package.json fileUtils of the project excluding node_modules and gen/ directories func (exec *Execute) FindPackageJSONFiles() []string { unfilteredListOfPackageJSONFiles, _ := exec.Utils.Glob("**/package.json") var packageJSONFiles []string for _, file := range unfilteredListOfPackageJSONFiles { if strings.Contains(file, "node_modules") { continue } if strings.HasPrefix(file, "gen"+string(os.PathSeparator)) || strings.Contains(file, string(os.PathSeparator)+"gen"+string(os.PathSeparator)) { continue } packageJSONFiles = append(packageJSONFiles, file) log.Entry().Info("Discovered package.json file " + file) } return packageJSONFiles } // FindPackageJSONFilesWithScript returns a list of package.json fileUtils that contain the script func (exec *Execute) FindPackageJSONFilesWithScript(packageJSONFiles []string, script string) ([]string, error) { var packagesWithScript []string for _, file := range packageJSONFiles { var packageJSON map[string]interface{} packageRaw, err := exec.Utils.FileRead(file) if err != nil { return nil, fmt.Errorf("failed to read %s to check for existence of %s script: %w", file, script, err) } err = json.Unmarshal(packageRaw, &packageJSON) if err != nil { return nil, fmt.Errorf("failed to unmarshal %s to check for existence of %s script: %w", file, script, err) } scripts, ok := packageJSON["scripts"].(map[string]interface{}) if ok { _, ok := scripts[script].(string) if ok { packagesWithScript = append(packagesWithScript, file) log.Entry().Info("Discovered " + script + " script in " + file) } } } return packagesWithScript, nil } // InstallAllDependencies executes npm or yarn Install for all package.json fileUtils defined in packageJSONFiles func (exec *Execute) InstallAllDependencies(packageJSONFiles []string) error { for _, packageJSON := range packageJSONFiles { err := exec.install(packageJSON) if err != nil { return err } } return nil } // install executes npm or yarn Install for package.json func (exec *Execute) install(packageJSON string) error { execRunner := exec.Utils.GetExecRunner() oldWorkingDirectory, err := exec.Utils.Getwd() if err != nil { return fmt.Errorf("failed to get current working directory before executing npm scripts: %w", err) } dir := filepath.Dir(packageJSON) err = exec.Utils.Chdir(dir) if err != nil { return fmt.Errorf("failed to change into directory for executing script: %w", err) } err = exec.SetNpmRegistries() if err != nil { return err } packageLockExists, yarnLockExists, err := exec.checkIfLockFilesExist() if err != nil { return err } log.Entry().WithField("WorkingDirectory", dir).Info("Running Install") if packageLockExists { err = execRunner.RunExecutable("npm", "ci") if err != nil { return err } } else if yarnLockExists { err = execRunner.RunExecutable("yarn", "install", "--frozen-lockfile") if err != nil { return err } } else { log.Entry().Warn("No package lock file found. " + "It is recommended to create a `package-lock.json` file by running `npm Install` locally." + " Add this file to your version control. " + "By doing so, the builds of your application become more reliable.") err = execRunner.RunExecutable("npm", "install") if err != nil { return err } } err = exec.Utils.Chdir(oldWorkingDirectory) if err != nil { return fmt.Errorf("failed to change back into original directory: %w", err) } return nil } // checkIfLockFilesExist checks if yarn/package lock fileUtils exist func (exec *Execute) checkIfLockFilesExist() (bool, bool, error) { packageLockExists, err := exec.Utils.FileExists("package-lock.json") if err != nil { return false, false, err } yarnLockExists, err := exec.Utils.FileExists("yarn.lock") if err != nil { return false, false, err } return packageLockExists, yarnLockExists, nil }