2020-05-20 13:41:23 +02:00
package npm
import (
"bytes"
2020-06-18 17:30:17 +02:00
"encoding/json"
"fmt"
2020-05-20 13:41:23 +02:00
"io"
2020-06-18 17:30:17 +02:00
"path/filepath"
2020-05-20 13:41:23 +02:00
"strings"
2021-03-04 11:16:59 +02:00
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
2020-05-20 13:41:23 +02:00
)
2020-06-18 17:30:17 +02:00
// 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
2020-07-16 17:16:55 +02:00
FindPackageJSONFilesWithExcludes ( excludeList [ ] string ) ( [ ] string , error )
2020-06-18 17:30:17 +02:00
FindPackageJSONFilesWithScript ( packageJSONFiles [ ] string , script string ) ( [ ] string , error )
2020-11-04 17:20:26 +02:00
RunScriptsInAllPackages ( runScripts [ ] string , runOptions [ ] string , scriptOptions [ ] string , virtualFrameBuffer bool , excludeList [ ] string , packagesList [ ] string ) error
2020-06-18 17:30:17 +02:00
InstallAllDependencies ( packageJSONFiles [ ] string ) error
SetNpmRegistries ( ) error
2021-03-04 11:16:59 +02:00
CreateBOM ( packageJSONFiles [ ] string ) error
2020-06-18 17:30:17 +02:00
}
// ExecutorOptions holds common parameters for functions of Executor
type ExecutorOptions struct {
2020-05-20 13:41:23 +02:00
DefaultNpmRegistry string
2020-06-18 17:30:17 +02:00
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 ,
}
2020-05-20 13:41:23 +02:00
}
2020-06-18 17:30:17 +02:00
// ExecRunner interface to enable mocking for testing
type ExecRunner interface {
SetEnv ( e [ ] string )
2020-05-20 13:41:23 +02:00
Stdout ( out io . Writer )
2020-06-18 17:30:17 +02:00
Stderr ( out io . Writer )
2020-05-20 13:41:23 +02:00
RunExecutable ( executable string , params ... string ) error
2020-06-18 17:30:17 +02:00
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
2020-05-20 13:41:23 +02:00
}
// SetNpmRegistries configures the given npm registries.
// CAUTION: This will change the npm configuration in the user's home directory.
2020-06-18 17:30:17 +02:00
func ( exec * Execute ) SetNpmRegistries ( ) error {
execRunner := exec . Utils . GetExecRunner ( )
2020-05-20 13:41:23 +02:00
const npmRegistry = "registry"
2020-08-11 15:58:39 +02:00
var buffer bytes . Buffer
execRunner . Stdout ( & buffer )
err := execRunner . RunExecutable ( "npm" , "config" , "get" , npmRegistry )
execRunner . Stdout ( log . Writer ( ) )
if err != nil {
return err
}
preConfiguredRegistry := buffer . String ( )
2020-05-20 13:41:23 +02:00
2020-08-11 15:58:39 +02:00
if registryIsNonEmpty ( preConfiguredRegistry ) {
log . Entry ( ) . Info ( "Discovered pre-configured npm registry " + npmRegistry + " with value " + preConfiguredRegistry )
}
2020-05-20 13:41:23 +02:00
2020-08-11 15:58:39 +02:00
if exec . Options . DefaultNpmRegistry != "" && registryRequiresConfiguration ( preConfiguredRegistry , "https://registry.npmjs.org" ) {
log . Entry ( ) . Info ( "npm registry " + npmRegistry + " was not configured, setting it to " + exec . Options . DefaultNpmRegistry )
err = execRunner . RunExecutable ( "npm" , "config" , "set" , npmRegistry , exec . Options . DefaultNpmRegistry )
if err != nil {
return err
2020-05-20 13:41:23 +02:00
}
}
2020-08-11 15:58:39 +02:00
2020-05-20 13:41:23 +02:00
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 )
}
2020-06-18 17:30:17 +02:00
// RunScriptsInAllPackages runs all scripts defined in ExecuteOptions.RunScripts
2020-11-04 17:20:26 +02:00
func ( exec * Execute ) RunScriptsInAllPackages ( runScripts [ ] string , runOptions [ ] string , scriptOptions [ ] string , virtualFrameBuffer bool , excludeList [ ] string , packagesList [ ] string ) error {
var packageJSONFiles [ ] string
var err error
if len ( packagesList ) > 0 {
packageJSONFiles = packagesList
} else {
packageJSONFiles , err = exec . FindPackageJSONFilesWithExcludes ( excludeList )
if err != nil {
return err
}
2020-07-16 17:16:55 +02:00
}
2020-06-18 17:30:17 +02:00
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 {
2020-07-09 14:57:41 +02:00
err = exec . executeScript ( packageJSON , script , runOptions , scriptOptions )
2020-06-18 17:30:17 +02:00
if err != nil {
return err
}
}
}
return nil
}
2020-07-09 14:57:41 +02:00
func ( exec * Execute ) executeScript ( packageJSON string , script string , runOptions [ ] string , scriptOptions [ ] string ) error {
2020-06-18 17:30:17 +02:00
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 ... )
}
2020-07-09 14:57:41 +02:00
if len ( scriptOptions ) > 0 {
npmRunArgs = append ( npmRunArgs , "--" )
npmRunArgs = append ( npmRunArgs , scriptOptions ... )
}
2020-06-18 17:30:17 +02:00
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
}
2020-07-16 17:16:55 +02:00
// FindPackageJSONFiles returns a list of all package.json files of the project excluding node_modules and gen/ directories
2020-06-18 17:30:17 +02:00
func ( exec * Execute ) FindPackageJSONFiles ( ) [ ] string {
2020-07-16 17:16:55 +02:00
packageJSONFiles , _ := exec . FindPackageJSONFilesWithExcludes ( [ ] string { } )
return packageJSONFiles
}
// FindPackageJSONFilesWithExcludes returns a list of all package.json files of the project excluding node_modules, gen/ and directories/patterns defined by excludeList
func ( exec * Execute ) FindPackageJSONFilesWithExcludes ( excludeList [ ] string ) ( [ ] string , error ) {
2020-06-18 17:30:17 +02:00
unfilteredListOfPackageJSONFiles , _ := exec . Utils . Glob ( "**/package.json" )
2020-07-16 17:16:55 +02:00
nodeModulesExclude := "**/node_modules/**"
genExclude := "**/gen/**"
excludeList = append ( excludeList , nodeModulesExclude , genExclude )
2020-11-26 12:45:53 +02:00
packageJSONFiles , err := piperutils . ExcludeFiles ( unfilteredListOfPackageJSONFiles , excludeList )
if err != nil {
return nil , err
}
2020-06-18 17:30:17 +02:00
2020-11-26 12:45:53 +02:00
for _ , file := range packageJSONFiles {
2020-06-18 17:30:17 +02:00
log . Entry ( ) . Info ( "Discovered package.json file " + file )
}
2020-07-16 17:16:55 +02:00
return packageJSONFiles , nil
2020-06-18 17:30:17 +02:00
}
// 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 {
2020-11-04 17:20:26 +02:00
fileExists , err := exec . Utils . FileExists ( packageJSON )
if err != nil {
return fmt . Errorf ( "cannot check if '%s' exists: %w" , packageJSON , err )
}
if ! fileExists {
return fmt . Errorf ( "package.json file '%s' not found: %w" , packageJSON , err )
}
err = exec . install ( packageJSON )
2020-06-18 17:30:17 +02:00
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
}
2021-03-04 11:16:59 +02:00
// CreateBOM generates BOM file using CycloneDX from all package.json files
func ( exec * Execute ) CreateBOM ( packageJSONFiles [ ] string ) error {
execRunner := exec . Utils . GetExecRunner ( )
// Install CycloneDX Node.js module locally without saving in package.json
err := execRunner . RunExecutable ( "npm" , "install" , "@cyclonedx/bom" , "--no-save" )
if err != nil {
return err
}
if len ( packageJSONFiles ) > 0 {
path := filepath . Dir ( packageJSONFiles [ 0 ] )
createBOMConfig := [ ] string {
"--schema" , "1.2" , // Target schema version
"--include-license-text" , "false" ,
"--include-dev" , "false" , // Include devDependencies
"--output" , "bom.xml" ,
}
params := [ ] string {
"cyclonedx-bom" ,
path ,
}
params = append ( params , createBOMConfig ... )
// Generate BOM from first package.json
err := execRunner . RunExecutable ( "npx" , params ... )
if err != nil {
return err
}
// Merge BOM(s) into the current BOM
for _ , packageJSONFile := range packageJSONFiles [ 1 : ] {
path := filepath . Dir ( packageJSONFile )
params = [ ] string {
"cyclonedx-bom" ,
path ,
"--append" ,
"bom.xml" ,
}
params = append ( params , createBOMConfig ... )
err := execRunner . RunExecutable ( "npx" , params ... )
if err != nil {
return err
}
}
}
return nil
}