github.com/saucelabs/saucectl@v0.175.1/internal/node/package.go (about) 1 package node 2 3 import ( 4 "encoding/json" 5 "os" 6 "path/filepath" 7 8 "github.com/rs/zerolog/log" 9 ) 10 11 type Package struct { 12 Dependencies map[string]string `json:"dependencies,omitempty"` 13 DevDependencies map[string]string `json:"devDependencies,omitempty"` 14 PeerDependencies map[string]string `json:"peerDependencies,omitempty"` 15 } 16 17 // permExcludes is a list of packages we never want to resolve, because they are provided by Sauce Labs. 18 var permExcludes = []string{ 19 "cypress", 20 "playwright", 21 "playwright-core", 22 "@playwright/test", 23 "testcafe", 24 } 25 26 func isPkgExcluded(pkg string) bool { 27 for _, p := range permExcludes { 28 if p == pkg { 29 return true 30 } 31 } 32 return false 33 } 34 35 func PackageFromFile(filename string) (Package, error) { 36 var p Package 37 38 fd, err := os.Open(filename) 39 if err != nil { 40 return p, err 41 } 42 defer fd.Close() 43 44 err = json.NewDecoder(fd).Decode(&p) 45 return p, err 46 } 47 48 // Requirements returns a list of all root dependencies for the given packages, including themselves. 49 func Requirements(root string, pkgs ...string) []string { 50 rootDeps := make(map[string]struct{}) 51 52 log.Info().Msgf("Getting requirements for npm dependencies %s", pkgs) 53 for _, pkg := range pkgs { 54 requirements(rootDeps, root, pkg, true) 55 } 56 57 var deps []string 58 for k := range rootDeps { 59 deps = append(deps, k) 60 } 61 return deps 62 } 63 64 // requirements recursively traverses the dependency tree for the given package, adding all root (shared) dependencies 65 // to the rootDeps map. 66 func requirements(rootDeps map[string]struct{}, root, pkg string, rootDep bool) { 67 if _, ok := rootDeps[pkg]; ok { 68 return 69 } 70 71 if isPkgExcluded(pkg) { 72 log.Info().Msgf("Skipping dependency %s, as it's provided by Sauce Labs", pkg) 73 return 74 } 75 76 log.Debug().Msgf("Getting requirements for %s", pkg) 77 p, err := PackageFromFile(filepath.Join(root, pkg, "package.json")) 78 if err != nil { 79 if os.IsNotExist(err) { 80 // likely an unmet peer dependency, not necessarily an issue 81 log.Debug().Msgf("Unmet requirement %s", pkg) 82 return 83 } 84 log.Err(err).Msgf("Failed to get requirements for %s", pkg) 85 return 86 } 87 if rootDep { 88 rootDeps[pkg] = struct{}{} 89 } 90 91 var requiredDeps []string 92 for k := range p.Dependencies { 93 requiredDeps = append(requiredDeps, k) 94 } 95 for k := range p.PeerDependencies { 96 requiredDeps = append(requiredDeps, k) 97 } 98 99 for _, v := range requiredDeps { 100 // check for nested dependencies (deps that conflict with requirements, hence embedded in a nested node_modules) 101 submodule := filepath.Join(pkg, "node_modules", v) 102 _, err = os.Stat(filepath.Join(root, submodule)) 103 if os.IsNotExist(err) { 104 // doesn't exist, so must be a root dependency 105 requirements(rootDeps, root, v, true) 106 continue 107 } 108 109 if err != nil { 110 log.Err(err).Msgf("Failed to inspect dependencies for %s", pkg) 111 continue 112 } 113 114 // Dependency exists in a nested node_modules. We don't need to add it to the list of requirements, but we do 115 // need to recurse into the node_modules to get its dependencies, since those might refer to other root deps. 116 requirements(rootDeps, root, submodule, false) 117 } 118 }