diff --git a/internal/resolution/pm/yarn/job.go b/internal/resolution/pm/yarn/job.go index 929b918f..16558b2f 100644 --- a/internal/resolution/pm/yarn/job.go +++ b/internal/resolution/pm/yarn/job.go @@ -1,12 +1,22 @@ package yarn import ( + "regexp" + "strings" + "github.com/debricked/cli/internal/resolution/job" "github.com/debricked/cli/internal/resolution/pm/util" ) const ( - yarn = "yarn" + yarn = "yarn" + invalidJsonErrRegex = "error SyntaxError.*package.json: (.*)" + invalidSchemaErrRegex = "error package.json: (.*)" + invalidArgumentErrRegex = "error TypeError \\[\\w+\\]: (.*)" + versionNotFoundErrRegex = "error (Couldn\\'t find any versions for .*)" + dependencyNotFoundErrRegex = `error.*? "?(https?://[^"\s:]+)?: Not found` + registryUnavailableErrRegex = "error Error: getaddrinfo ENOTFOUND ([\\w\\.]+)" + permissionDeniedErrRegex = "Error: (.*): Request failed \"404 Not Found\"" ) type Job struct { @@ -34,31 +44,181 @@ func (j *Job) Install() bool { func (j *Job) Run() { if j.install { + status := "installing dependencies" + j.SendStatus(status) + j.yarnCommand = yarn + + installCmd, err := j.cmdFactory.MakeInstallCmd(j.yarnCommand, j.GetFile()) - j.SendStatus("installing dependencies") - _, err := j.runInstallCmd() if err != nil { - jobError := util.NewPMJobError(err.Error()) - j.Errors().Critical(jobError) + j.handleError(j.createError(err.Error(), installCmd.String(), status)) + + return + } + + if output, err := installCmd.Output(); err != nil { + error := strings.Join([]string{string(output), j.GetExitError(err).Error()}, "") + j.handleError(j.createError(error, installCmd.String(), status)) return } } +} + +func (j *Job) createError(error string, cmd string, status string) job.IError { + cmdError := util.NewPMJobError(error) + cmdError.SetCommand(cmd) + cmdError.SetStatus(status) + return cmdError } -func (j *Job) runInstallCmd() ([]byte, error) { +func (j *Job) handleError(cmdError job.IError) { + expressions := []string{ + invalidJsonErrRegex, + invalidSchemaErrRegex, + invalidArgumentErrRegex, + versionNotFoundErrRegex, + dependencyNotFoundErrRegex, + registryUnavailableErrRegex, + permissionDeniedErrRegex, + } + + for _, expression := range expressions { + regex := regexp.MustCompile(expression) + matches := regex.FindAllStringSubmatch(cmdError.Error(), -1) + + if len(matches) > 0 { + cmdError = j.addDocumentation(expression, matches, cmdError) + j.Errors().Append(cmdError) + + return + } + } + + j.Errors().Append(cmdError) +} + +func (j *Job) addDocumentation(expr string, matches [][]string, cmdError job.IError) job.IError { + documentation := cmdError.Documentation() + + switch { + case expr == invalidJsonErrRegex: + documentation = getInvalidJsonErrorDocumentation(matches) + case expr == invalidSchemaErrRegex: + documentation = getInvalidSchemaErrorDocumentation(matches) + case expr == invalidArgumentErrRegex: + documentation = getInvalidArgumentErrorDocumentation(matches) + case expr == versionNotFoundErrRegex: + documentation = getVersionNotFoundErrorDocumentation(matches) + case expr == dependencyNotFoundErrRegex: + documentation = getDependencyNotFoundErrorDocumentation(matches) + case expr == registryUnavailableErrRegex: + documentation = getRegistryUnavailableErrorDocumentation(matches) + case expr == permissionDeniedErrRegex: + documentation = getPermissionDeniedErrorDocumentation(matches) + } + + cmdError.SetDocumentation(documentation) + + return cmdError +} - j.yarnCommand = yarn - installCmd, err := j.cmdFactory.MakeInstallCmd(j.yarnCommand, j.GetFile()) - if err != nil { - return nil, err +func getInvalidJsonErrorDocumentation(matches [][]string) string { + message := "" + if len(matches) > 0 && len(matches[0]) > 1 { + message = matches[0][1] } - installCmdOutput, err := installCmd.Output() - if err != nil { - return nil, j.GetExitError(err) + return strings.Join( + []string{ + "Your package.json file contains invalid JSON:", + message + ".", + }, " ") +} + +func getInvalidSchemaErrorDocumentation(matches [][]string) string { + message := "" + if len(matches) > 0 && len(matches[0]) > 1 { + message = matches[0][1] + } + + return strings.Join( + []string{ + "Your package.json file is not valid:", + message + ".", + "Please make sure it follows the schema.", + }, " ") +} + +func getInvalidArgumentErrorDocumentation(matches [][]string) string { + message := "" + if len(matches) > 0 && len(matches[0]) > 1 { + message = matches[0][1] + } + + return strings.Join( + []string{ + message + ".", + "Please make sure that your package.json file doesn't contain errors.", + }, " ") +} + +func getDependencyNotFoundErrorDocumentation(matches [][]string) string { + dependency := "" + if len(matches) > 0 && len(matches[0]) > 1 { + dependency = matches[0][1] + } + + return strings.Join( + []string{ + "Failed to find package", + "\"" + dependency + "\"", + "that satisfies the requirement from yarn dependencies.", + "Please check that dependencies are correct in your package.json file.", + "\n" + util.InstallPrivateDependencyMessage, + }, " ") +} + +func getVersionNotFoundErrorDocumentation(matches [][]string) string { + message := "" + if len(matches) > 0 && len(matches[0]) > 1 { + message = matches[0][1] + } + + return strings.Join( + []string{ + message + ".", + "Please check that dependencies are correct in your package.json file.", + }, " ") +} + +func getRegistryUnavailableErrorDocumentation(matches [][]string) string { + registry := "" + if len(matches) > 0 && len(matches[0]) > 1 { + registry = matches[0][1] + } + + return strings.Join( + []string{ + "Package registry", + "\"" + registry + "\"", + "is not available at the moment.", + "There might be a trouble with your network connection.", + }, " ") +} + +func getPermissionDeniedErrorDocumentation(matches [][]string) string { + dependency := "" + if len(matches) > 0 && len(matches[0]) > 1 { + dependency = matches[0][1] } - return installCmdOutput, nil + return strings.Join( + []string{ + "Failed to find a package that satisfies requirements for yarn dependencies:", + dependency + ".", + "This could mean that the package or version does not exist or is private.\n", + util.InstallPrivateDependencyMessage, + }, " ") } diff --git a/internal/resolution/pm/yarn/job_test.go b/internal/resolution/pm/yarn/job_test.go index 47b5da48..783a6f45 100644 --- a/internal/resolution/pm/yarn/job_test.go +++ b/internal/resolution/pm/yarn/job_test.go @@ -22,16 +22,6 @@ func TestNewJob(t *testing.T) { assert.False(t, j.Errors().HasError()) } -func TestRunInstall(t *testing.T) { - cmdFactoryMock := testdata.NewEchoCmdFactory() - j := NewJob("file", false, cmdFactoryMock) - - _, err := j.runInstallCmd() - assert.NoError(t, err) - - assert.False(t, j.Errors().HasError()) -} - func TestInstall(t *testing.T) { j := Job{install: true} assert.Equal(t, true, j.Install()) @@ -41,16 +31,70 @@ func TestInstall(t *testing.T) { } func TestRunInstallCmdErr(t *testing.T) { - cmdErr := errors.New("cmd-error") - cmdFactoryMock := testdata.NewEchoCmdFactory() - cmdFactoryMock.MakeInstallErr = cmdErr - j := NewJob("file", true, cmdFactoryMock) + cases := []struct { + cmd string + error string + doc string + }{ + { + error: "cmd-error", + doc: util.UnknownError, + }, + { + error: "error SyntaxError: /home/asus/Projects/playground/rpn_js/package.json: Unexpected string in JSON at position 186\n at JSON.parse ()", + doc: "Your package.json file contains invalid JSON: Unexpected string in JSON at position 186.", + }, + { + error: "error package.json: \"name\" is not a string", + doc: "Your package.json file is not valid: \"name\" is not a string. Please make sure it follows the schema.", + }, + { + error: "error TypeError [ERR_INVALID_ARG_TYPE]: The \"path\" argument must be of type string. Received an instance of Array\n at validateString (internal/validators.js:120:11)\n", + doc: "The \"path\" argument must be of type string. Received an instance of Array. Please make sure that your package.json file doesn't contain errors.", + }, + { + error: "error Error: https://registry.yarnpkg.com/chalke: Not found\n at Request.params.callback [as _callback] (/usr/local/lib/node_modules/yarn/lib/cli.js:66148:18)", + doc: "Failed to find package \"https://registry.yarnpkg.com/chalke\" that satisfies the requirement from yarn dependencies. Please check that dependencies are correct in your package.json file. \nIf this is a private dependency, please make sure that the debricked CLI has access to install it or pre-install it before running the debricked CLI.", + }, + { + error: `error An unexpected error occurred: "https://registry.yarnpkg.com/chalke: Not found".`, + doc: "Failed to find package \"https://registry.yarnpkg.com/chalke\" that satisfies the requirement from yarn dependencies. Please check that dependencies are correct in your package.json file. \nIf this is a private dependency, please make sure that the debricked CLI has access to install it or pre-install it before running the debricked CLI.", + }, + { + error: "error Couldn't find any versions for \"chalk\" that matches \"^300.0.0\"\ninfo Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.", + doc: "Couldn't find any versions for \"chalk\" that matches \"^300.0.0\". Please check that dependencies are correct in your package.json file.", + }, + { + error: "error Error: getaddrinfo ENOTFOUND nexus.dev\n at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:66:26)\n", + doc: "Package registry \"nexus.dev\" is not available at the moment. There might be a trouble with your network connection.", + }, + { + error: "Error: https://registry.npmjs.org/@private/my-private-package/-/my-private-package-0.0.5.tgz: Request failed \"404 Not Found\"", + doc: "Failed to find a package that satisfies requirements for yarn dependencies: https://registry.npmjs.org/@private/my-private-package/-/my-private-package-0.0.5.tgz. This could mean that the package or version does not exist or is private.\n If this is a private dependency, please make sure that the debricked CLI has access to install it or pre-install it before running the debricked CLI.", + }, + } - go jobTestdata.WaitStatus(j) - j.Run() + for _, c := range cases { + cmdErr := errors.New(c.error) + cmdFactoryMock := testdata.NewEchoCmdFactory() + cmdFactoryMock.MakeInstallErr = cmdErr + cmd, _ := cmdFactoryMock.MakeInstallCmd("echo", "package.json") + + expectedError := util.NewPMJobError(c.error) + expectedError.SetDocumentation(c.doc) + expectedError.SetStatus("installing dependencies") + expectedError.SetCommand(cmd.String()) + + j := NewJob("file", true, cmdFactoryMock) + + go jobTestdata.WaitStatus(j) + j.Run() + + errors := j.Errors().GetAll() - assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(cmdErr.Error())) + assert.Len(t, j.Errors().GetAll(), 1) + assert.Contains(t, errors, expectedError) + } } func TestRunInstallCmdOutputErr(t *testing.T) {