Skip to content

Commit

Permalink
Yarn error enhancements (#168)
Browse files Browse the repository at this point in the history
* Enhance Yarn errors

* Improve document functions

* Fix Yarn error parsing

* Fix dependency not found error message format

---------

Co-authored-by: Emil Wåreus <[email protected]>
  • Loading branch information
4ernovm and emilwareus authored Dec 19, 2023
1 parent f270a3f commit 3db6bb9
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 32 deletions.
188 changes: 174 additions & 14 deletions internal/resolution/pm/yarn/job.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
}, " ")
}
80 changes: 62 additions & 18 deletions internal/resolution/pm/yarn/job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 (<anonymous>)",
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) {
Expand Down

0 comments on commit 3db6bb9

Please sign in to comment.