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  }