github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/buildtools/yarn/parse.go (about)

     1  package yarn
     2  
     3  import (
     4  	"errors"
     5  	"path/filepath"
     6  	"regexp"
     7  
     8  	yaml "gopkg.in/yaml.v2"
     9  
    10  	"github.com/fossas/fossa-cli/buildtools/npm"
    11  	"github.com/fossas/fossa-cli/files"
    12  	"github.com/fossas/fossa-cli/graph"
    13  	"github.com/fossas/fossa-cli/pkg"
    14  )
    15  
    16  // FromProject builds a dependency graph based on the provided lockfile and manifest
    17  func FromProject(manifestPath string, lockFilePath string) (graph.Deps, error) {
    18  	rootPackageJsonExists, err := files.Exists(manifestPath)
    19  	if err != nil {
    20  		return graph.Deps{}, err
    21  	}
    22  	if !rootPackageJsonExists {
    23  		return graph.Deps{}, errors.New(manifestPath + " does not exist")
    24  	}
    25  
    26  	yarnLockfileExists, err := files.Exists(lockFilePath)
    27  	if err != nil {
    28  		return graph.Deps{}, err
    29  	}
    30  	if !yarnLockfileExists {
    31  		return graph.Deps{}, errors.New(lockFilePath + " does not exist")
    32  	}
    33  
    34  	// To know which deps are direct, we need the manifest def
    35  	lockfile, err := readLockfile(lockFilePath)
    36  	if err != nil {
    37  		return graph.Deps{}, err
    38  	}
    39  
    40  	manifest, err := npm.FromManifest(manifestPath)
    41  	if err != nil {
    42  		return graph.Deps{}, err
    43  	}
    44  
    45  	directDeps := make(pkg.Imports, len(manifest.Dependencies))
    46  	i := 0
    47  	for name, revision := range manifest.Dependencies {
    48  		directDeps[i] = pkg.Import{
    49  			Target: name,
    50  			Resolved: pkg.ID{
    51  				Name:     name,
    52  				Revision: lockfile.resolve(name, revision),
    53  				Type:     pkg.NodeJS,
    54  			},
    55  		}
    56  		i++
    57  	}
    58  
    59  	transitiveDepGraph := make(map[pkg.ID]pkg.Package)
    60  	lockfile.resolveDepGraph(manifest.Dependencies, transitiveDepGraph)
    61  
    62  	return graph.Deps{
    63  		Direct:     directDeps,
    64  		Transitive: transitiveDepGraph,
    65  	}, nil
    66  }
    67  
    68  func readLockfile(pathElems ...string) (yarnLockfile, error) {
    69  	var lockfile yarnLockfile
    70  
    71  	filePath := filepath.Join(pathElems...)
    72  
    73  	fileContent, err := files.Read(filePath)
    74  	if err != nil {
    75  		return yarnLockfile{}, err
    76  	}
    77  
    78  	r, err := regexp.Compile("\\s\"")
    79  	if err != nil {
    80  		return yarnLockfile{}, err
    81  	}
    82  	yamlCompatLockfile := r.ReplaceAll(fileContent, []byte(": \""))
    83  
    84  	err = yaml.Unmarshal(yamlCompatLockfile, &lockfile)
    85  	if err != nil {
    86  		return yarnLockfile{}, err
    87  	}
    88  
    89  	return lockfile, nil
    90  }
    91  
    92  type lockfileEntry struct {
    93  	// resolved version for the particular entry based on the provided semver revision
    94  	Version string
    95  	// the list of unresolved modules and revisions (e.g. type-detect : ^4.0.0)
    96  	Dependencies map[string]string
    97  }
    98  
    99  // Keys for each lockfile entry follow the schema <moduleName>@<semverVersion>  (e.g. type-detect@^4.0.0)
   100  type yarnLockfile map[string]lockfileEntry
   101  
   102  func (l yarnLockfile) resolve(moduleName string, unresolvedRevision string) string {
   103  	return l[moduleName+"@"+unresolvedRevision].Version
   104  }
   105  
   106  func (l yarnLockfile) resolveDepGraph(unresolvedDirectDeps map[string]string, depGraph map[pkg.ID]pkg.Package) {
   107  	for directDepName, unresolvedVersion := range unresolvedDirectDeps {
   108  		lockfileKey := directDepName + "@" + unresolvedVersion
   109  
   110  		entry := l[lockfileKey]
   111  
   112  		// Create package key for current dep
   113  		pkgID := pkg.ID{
   114  			Name:     directDepName,
   115  			Revision: entry.Version,
   116  			Type:     pkg.NodeJS,
   117  		}
   118  
   119  		// Build direct deps for current import
   120  		imports := make(pkg.Imports, len(entry.Dependencies))
   121  		i := 0
   122  		for name, semverRevision := range entry.Dependencies {
   123  			imports[i] = pkg.Import{
   124  				Target: name,
   125  				Resolved: pkg.ID{
   126  					Name:     name,
   127  					Revision: l.resolve(name, semverRevision),
   128  					Type:     pkg.NodeJS,
   129  				},
   130  			}
   131  			i++
   132  		}
   133  
   134  		// Add new package to dep graph
   135  		depGraph[pkgID] = pkg.Package{
   136  			ID:       pkgID,
   137  			Imports:  imports,
   138  			Strategy: "yarn-lockfile",
   139  		}
   140  
   141  		// recurse for the n-2 direct deps
   142  		l.resolveDepGraph(entry.Dependencies, depGraph)
   143  	}
   144  }