github.com/SAP/jenkins-library@v1.362.0/cmd/npmExecuteLint.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/http"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/SAP/jenkins-library/pkg/command"
    13  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    14  	"github.com/SAP/jenkins-library/pkg/log"
    15  	"github.com/SAP/jenkins-library/pkg/npm"
    16  	"github.com/SAP/jenkins-library/pkg/piperutils"
    17  	"github.com/SAP/jenkins-library/pkg/telemetry"
    18  )
    19  
    20  type lintUtils interface {
    21  	Glob(pattern string) (matches []string, err error)
    22  
    23  	getExecRunner() command.ExecRunner
    24  	getGeneralPurposeConfig(configURL string)
    25  }
    26  
    27  type lintUtilsBundle struct {
    28  	*piperutils.Files
    29  	execRunner *command.Command
    30  	client     *piperhttp.Client
    31  }
    32  
    33  func newLintUtilsBundle() *lintUtilsBundle {
    34  	return &lintUtilsBundle{
    35  		Files:  &piperutils.Files{},
    36  		client: &piperhttp.Client{},
    37  	}
    38  }
    39  
    40  func (u *lintUtilsBundle) getExecRunner() command.ExecRunner {
    41  	if u.execRunner == nil {
    42  		u.execRunner = &command.Command{}
    43  		u.execRunner.Stdout(log.Writer())
    44  		u.execRunner.Stderr(log.Writer())
    45  	}
    46  	return u.execRunner
    47  }
    48  
    49  func (u *lintUtilsBundle) getGeneralPurposeConfig(configURL string) {
    50  	response, err := u.client.SendRequest(http.MethodGet, configURL, nil, nil, nil)
    51  	if err != nil {
    52  		log.Entry().Warnf("failed to download general purpose configuration: %v", err)
    53  		return
    54  	}
    55  
    56  	defer response.Body.Close()
    57  
    58  	content, err := io.ReadAll(response.Body)
    59  	if err != nil {
    60  		log.Entry().Warnf("error while reading the general purpose configuration: %v", err)
    61  		return
    62  	}
    63  
    64  	err = u.FileWrite(filepath.Join(".pipeline", ".eslintrc.json"), content, os.ModePerm)
    65  	if err != nil {
    66  		log.Entry().Warnf("failed to write .eslintrc.json file to .pipeline/: %v", err)
    67  	}
    68  }
    69  
    70  func npmExecuteLint(config npmExecuteLintOptions, telemetryData *telemetry.CustomData) {
    71  	utils := newLintUtilsBundle()
    72  	npmExecutorOptions := npm.ExecutorOptions{DefaultNpmRegistry: config.DefaultNpmRegistry, ExecRunner: utils.getExecRunner()}
    73  	npmExecutor := npm.NewExecutor(npmExecutorOptions)
    74  
    75  	err := runNpmExecuteLint(npmExecutor, utils, &config)
    76  	if err != nil {
    77  		log.Entry().WithError(err).Fatal("step execution failed")
    78  	}
    79  }
    80  
    81  func runNpmExecuteLint(npmExecutor npm.Executor, utils lintUtils, config *npmExecuteLintOptions) error {
    82  	if len(config.RunScript) == 0 {
    83  		return fmt.Errorf("runScript is not allowed to be empty!")
    84  	}
    85  
    86  	packageJSONFiles := npmExecutor.FindPackageJSONFiles()
    87  	packagesWithLintScript, _ := npmExecutor.FindPackageJSONFilesWithScript(packageJSONFiles, config.RunScript)
    88  
    89  	if len(packagesWithLintScript) > 0 {
    90  		if config.Install {
    91  			err := npmExecutor.InstallAllDependencies(packagesWithLintScript)
    92  			if err != nil {
    93  				return err
    94  			}
    95  		}
    96  
    97  		err := runLintScript(npmExecutor, config.RunScript, config.FailOnError)
    98  		if err != nil {
    99  			return err
   100  		}
   101  	} else {
   102  		if config.Install {
   103  			err := npmExecutor.InstallAllDependencies(packageJSONFiles)
   104  			if err != nil {
   105  				return err
   106  			}
   107  		}
   108  
   109  		err := runDefaultLint(npmExecutor, utils, config.FailOnError, config.OutputFormat, config.OutputFileName)
   110  
   111  		if err != nil {
   112  			return err
   113  		}
   114  	}
   115  	return nil
   116  }
   117  
   118  func runLintScript(npmExecutor npm.Executor, runScript string, failOnError bool) error {
   119  	runScripts := []string{runScript}
   120  	runOptions := []string{"--silent"}
   121  
   122  	err := npmExecutor.RunScriptsInAllPackages(runScripts, runOptions, nil, false, nil, nil)
   123  	if err != nil {
   124  		if failOnError {
   125  			return fmt.Errorf("%s script execution failed with error: %w. This might be the result of severe linting findings, or some other issue while executing the script. Please examine the linting results in the UI, the cilint.xml file, if available, or the log above. ", runScript, err)
   126  		}
   127  	}
   128  	return nil
   129  }
   130  
   131  func runDefaultLint(npmExecutor npm.Executor, utils lintUtils, failOnError bool, outputFormat string, outputFileName string) error {
   132  	execRunner := utils.getExecRunner()
   133  	eslintConfigs := findEslintConfigs(utils)
   134  
   135  	err := npmExecutor.SetNpmRegistries()
   136  	if err != nil {
   137  		log.Entry().Warnf("failed to set npm registries before running default lint: %v", err)
   138  	}
   139  
   140  	// If the user has ESLint configs in the project we use them to lint existing JS files. In this case we do not lint other types of files,
   141  	// i.e., .jsx, .ts, .tsx, since we can not be sure that the provided config enables parsing of these file types.
   142  	if len(eslintConfigs) > 0 {
   143  		for i, config := range eslintConfigs {
   144  			lintPattern := "."
   145  			dir := filepath.Dir(config)
   146  			if dir != "." {
   147  				lintPattern = dir + "/**/*.js"
   148  			}
   149  
   150  			args := prepareArgs([]string{
   151  				"eslint",
   152  				lintPattern,
   153  				"-f", outputFormat,
   154  				"--ignore-pattern", "node_modules/",
   155  				"--ignore-pattern", ".eslintrc.js",
   156  			}, fmt.Sprintf("./%s_%%s", strconv.Itoa(i)), outputFileName)
   157  
   158  			err = execRunner.RunExecutable("npx", args...)
   159  			if err != nil {
   160  				if failOnError {
   161  					return fmt.Errorf("Lint execution failed. This might be the result of severe linting findings, problems with the provided ESLint configuration (%s), or another issue. Please examine the linting results in the UI or in %s, if available, or the log above. ", config, strconv.Itoa(i)+"_defaultlint.xml")
   162  				}
   163  			}
   164  		}
   165  	} else {
   166  		// install dependencies manually, since npx cannot resolve the dependencies required for general purpose
   167  		// ESLint config, e.g., TypeScript ESLint plugin
   168  		log.Entry().Info("Run ESLint with general purpose config")
   169  		generalPurposeLintConfigURI := "https://raw.githubusercontent.com/SAP/jenkins-library/master/resources/.eslintrc.json"
   170  		utils.getGeneralPurposeConfig(generalPurposeLintConfigURI)
   171  
   172  		err = execRunner.RunExecutable("npm", "install", "eslint@^7.0.0", "typescript@^3.7.4", "@typescript-eslint/parser@^3.0.0", "@typescript-eslint/eslint-plugin@^3.0.0")
   173  		if err != nil {
   174  			if failOnError {
   175  				return fmt.Errorf("linter installation failed: %s", err)
   176  			}
   177  		}
   178  
   179  		args := prepareArgs([]string{
   180  			"--no-install",
   181  			"eslint",
   182  			".",
   183  			"--ext", ".js,.jsx,.ts,.tsx",
   184  			"-c", ".pipeline/.eslintrc.json",
   185  			"-f", outputFormat,
   186  			"--ignore-pattern", ".eslintrc.js",
   187  		}, "./%s", outputFileName)
   188  
   189  		err = execRunner.RunExecutable("npx", args...)
   190  		if err != nil {
   191  			if failOnError {
   192  				return fmt.Errorf("lint execution failed. This might be the result of severe linting findings. The lint configuration used can be found here: %s", generalPurposeLintConfigURI)
   193  			}
   194  		}
   195  	}
   196  	return nil
   197  }
   198  
   199  func findEslintConfigs(utils lintUtils) []string {
   200  	unfilteredListOfEslintConfigs, err := utils.Glob("**/.eslintrc*")
   201  	if err != nil {
   202  		log.Entry().Warnf("Error during resolving lint config files: %v", err)
   203  	}
   204  	var eslintConfigs []string
   205  
   206  	for _, config := range unfilteredListOfEslintConfigs {
   207  		if strings.Contains(config, "node_modules") {
   208  			continue
   209  		}
   210  
   211  		if strings.HasPrefix(config, ".pipeline"+string(os.PathSeparator)) {
   212  			continue
   213  		}
   214  
   215  		eslintConfigs = append(eslintConfigs, config)
   216  		log.Entry().Info("Discovered ESLint config " + config)
   217  	}
   218  	return eslintConfigs
   219  }
   220  
   221  func prepareArgs(defaultArgs []string, outputFileNamePattern, outputFileName string) []string {
   222  	if outputFileName != "" { // in this case we omit the -o flag and output will go to the log
   223  		defaultArgs = append(defaultArgs, "-o", fmt.Sprintf(outputFileNamePattern, outputFileName))
   224  	}
   225  	return defaultArgs
   226  
   227  }