github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/analyzers/nodejs/nodejs.go (about) 1 // Package nodejs provides analyzers for Node.js projects. 2 // 3 // A Node.js project is defined as any folder with a `package.json`. A project 4 // may or may not have dependencies. 5 // 6 // A `BuildTarget` for Node.js is defined as the relative path to the directory 7 // containing the `package.json`, and the `Dir` is defined as the CWD for 8 // running build tools (like `npm` or `yarn`). 9 // 10 // `npm` and `yarn` are explicitly supported as first-class tools. Where 11 // possible, these tools are queried before falling back to other strategies. 12 // 13 // All Node.js projects are implicitly supported via `node_modules` parsing. 14 package nodejs 15 16 import ( 17 "os" 18 "path/filepath" 19 "strings" 20 21 "github.com/apex/log" 22 "github.com/mitchellh/mapstructure" 23 "github.com/pkg/errors" 24 25 "github.com/fossas/fossa-cli/buildtools/npm" 26 "github.com/fossas/fossa-cli/buildtools/yarn" 27 "github.com/fossas/fossa-cli/exec" 28 "github.com/fossas/fossa-cli/files" 29 "github.com/fossas/fossa-cli/graph" 30 "github.com/fossas/fossa-cli/module" 31 "github.com/fossas/fossa-cli/pkg" 32 ) 33 34 type Analyzer struct { 35 NodeVersion string 36 37 NPM npm.NPM 38 Yarn yarn.YarnTool 39 40 Module module.Module 41 Options Options 42 } 43 44 // Options contains options for the `Analyzer`. 45 // 46 // The analyzer can use many different strategies. These are: 47 // 48 // - `yarn`: Run and parse `yarn ls --json`. 49 // - `npm`: Run and parse `npm ls --json`. 50 // - `yarn.lock`: Parse `./yarn.lock`. 51 // - `package-lock.json`: Parse `./package-lock.json`. 52 // - `node_modules`: Parse `./package.json` and recursively look up 53 // dependencies with `node_modules` resolution. 54 // - `node_modules_local`: Parse manifests in `./node_modules``. 55 // - `package.json`: Parse `./package.json`. 56 // 57 // If no strategies are specified, the analyzer will try each of these 58 // strategies in descending order. 59 type Options struct { 60 Strategy string `mapstructure:"strategy"` 61 AllowNPMErr bool `mapstructure:"allow-npm-err"` 62 } 63 64 // New configures Node, NPM, and Yarn commands. 65 func New(m module.Module) (*Analyzer, error) { 66 log.WithField("options", m.Options).Debug("constructing analyzer") 67 68 _, nodeVersion, nodeErr := exec.Which("-v", os.Getenv("FOSSA_NODE_CMD"), "node", "nodejs") 69 if nodeErr != nil { 70 log.Warnf("Could not find Node.JS: %s", nodeErr.Error()) 71 } 72 73 var options Options 74 err := mapstructure.Decode(m.Options, &options) 75 if err != nil { 76 return nil, err 77 } 78 79 npmTool, err := npm.New() 80 if err != nil { 81 log.Warn("Could not initialize npm tooling") 82 } 83 84 yarnTool, err := yarn.New() 85 if err != nil { 86 log.Warn("Could not initialze yarn tooling") 87 } 88 89 analyzer := Analyzer{ 90 NodeVersion: nodeVersion, 91 92 NPM: npmTool, 93 Yarn: yarnTool, 94 95 Module: m, 96 Options: options, 97 } 98 99 log.Debugf("Initialized Node.js analyzer: %#v", analyzer) 100 return &analyzer, nil 101 } 102 103 // Discover searches for `package.json`s not within a `node_modules` or 104 // `bower_components`. 105 func Discover(dir string, options map[string]interface{}) ([]module.Module, error) { 106 log.WithField("dir", dir).Debug("discovering modules") 107 var modules []module.Module 108 err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 109 if err != nil { 110 log.WithError(err).WithField("path", path).Debug("error while walking for discovery") 111 return err 112 } 113 114 // Skip **/node_modules and **/bower_components 115 if info.IsDir() && (info.Name() == "node_modules" || info.Name() == "bower_components") { 116 log.Debugf("Skipping directory: %s", info.Name()) 117 return filepath.SkipDir 118 } 119 120 if !info.IsDir() && info.Name() == "package.json" { 121 name := filepath.Base(filepath.Dir(path)) 122 // Parse from project name from `package.json` if possible 123 if manifest, err := npm.FromManifest(path, "package.json"); err == nil && manifest.Name != "" { 124 name = manifest.Name 125 } 126 127 log.Debugf("Found NodeJS project: %s (%s)", path, name) 128 path, err = filepath.Rel(dir, path) 129 if err != nil { 130 panic(err) 131 } 132 modules = append(modules, module.Module{ 133 Name: name, 134 Type: pkg.NodeJS, 135 BuildTarget: filepath.Dir(path), 136 Dir: filepath.Dir(path), 137 }) 138 } 139 return nil 140 }) 141 142 if err != nil { 143 return nil, errors.Wrap(err, "could not find NodeJS projects") 144 } 145 146 return modules, nil 147 } 148 149 // Clean removes `node_modules`. 150 func (a *Analyzer) Clean() error { 151 return files.Rm(a.Module.Dir, "node_modules") 152 } 153 154 // Build runs `yarn install --production --frozen-lockfile` if there exists a 155 // `yarn.lock` and `yarn` is available. Otherwise, it runs 156 // `npm install --production`. 157 func (a *Analyzer) Build() error { 158 log.Debugf("Running Node.js build: %#v", a.Module) 159 160 // Prefer Yarn where possible. 161 if ok, err := files.Exists(a.Module.Dir, "yarn.lock"); err == nil && ok && a.Yarn.Exists() { 162 err := a.Yarn.Install(a.Module.Dir) 163 if err != nil { 164 return errors.Wrap(err, "could not run `yarn` build") 165 } 166 } else { 167 if !a.NPM.Exists() { 168 return errors.New("attempting to build using npm without npm tooling") 169 } 170 171 err := a.NPM.Install(a.Module.Dir) 172 if err != nil { 173 return errors.Wrap(err, "could not run `npm` build") 174 } 175 } 176 177 log.Debug("Done running Node.js build.") 178 return nil 179 } 180 181 // IsBuilt returns true if a project has a manifest and either has no 182 // dependencies or has a `node_modules` folder. 183 // 184 // Note that there could be very strange builds where this will produce false 185 // negatives (e.g. `node_modules` exists in a parent folder). There can also 186 // exist builds where this will produce false positives (e.g. `node_modules` 187 // folder does not include the correct dependencies). We also don't take 188 // $NODE_PATH into account during resolution. 189 // 190 // TODO: with significantly more effort, we can eliminate both of these 191 // situations. 192 func (a *Analyzer) IsBuilt() (bool, error) { 193 a.Module.BuildTarget = fixLegacyBuildTarget(a.Module.BuildTarget) 194 log.Debugf("Checking Node.js build: %#v", a.Module) 195 196 manifest, err := npm.FromManifest(a.Module.BuildTarget, "package.json") 197 if err != nil { 198 return false, errors.Wrap(err, "could not parse package manifest to check build") 199 } 200 201 if len(manifest.Dependencies) == 0 { 202 log.Debugf("Done checking Node.js build: project has no dependencies") 203 return true, nil 204 } 205 206 hasNodeModules, err := files.ExistsFolder(a.Module.Dir, "node_modules") 207 if err != nil { 208 return false, err 209 } 210 211 log.Debugf("Done checking Node.js build: %#v", hasNodeModules) 212 return hasNodeModules, nil 213 } 214 215 func (a *Analyzer) Analyze() (graph.Deps, error) { 216 a.Module.BuildTarget = fixLegacyBuildTarget(a.Module.BuildTarget) 217 log.Debugf("Running Nodejs analysis: %#v", a.Module) 218 // if npm as a tool does not exist, skip this 219 if a.NPM.Exists() { 220 pkgs, err := a.NPM.List(a.Module.BuildTarget) 221 if err == nil { 222 // TODO: we should move this functionality in to the buildtool, and have it 223 // return `pkg.Package`s. 224 // Set direct dependencies. 225 var imports []pkg.Import 226 for name, dep := range pkgs.Dependencies { 227 imports = append(imports, pkg.Import{ 228 Target: dep.From, 229 Resolved: pkg.ID{ 230 Type: pkg.NodeJS, 231 Name: name, 232 Revision: dep.Version, 233 Location: dep.Resolved, 234 }, 235 }) 236 } 237 238 // Set transitive dependencies. 239 deps := make(map[pkg.ID]pkg.Package) 240 recurseDeps(deps, pkgs) 241 242 log.Debugf("Done running Nodejs analysis: %#v", deps) 243 244 return graph.Deps{ 245 Direct: imports, 246 Transitive: deps, 247 }, nil 248 } 249 250 log.Warnf("NPM had non-zero exit code: %s", err.Error()) 251 log.Debug("Using fallback of node_modules") 252 } 253 254 deps, err := npm.FromNodeModules(a.Module.BuildTarget, "package.json") 255 if err == nil { 256 return deps, nil 257 } 258 259 log.Warnf("Could not determine deps from node_modules") 260 log.Debug("Using fallback of lockfile check") 261 262 // currently only support yarn.lock 263 return yarn.FromProject(filepath.Join(a.Module.BuildTarget, "package.json"), filepath.Join(a.Module.BuildTarget, "yarn.lock")) 264 } 265 266 // fixLegacyBuildTarget ensures that legacy behavior stays intact but users are warned if it is implemented. 267 func fixLegacyBuildTarget(target string) string { 268 if strings.HasSuffix(target, string(filepath.Separator)+"package.json") { 269 log.Warn("Specifying the package.json file as a module's target in fossa's config file is no longer supported. Instead, the target should specify the path to the project folder.") 270 target = strings.TrimSuffix(target, string(filepath.Separator)+"package.json") 271 } 272 if strings.HasSuffix(target, string(filepath.Separator)) { 273 log.Warn("Trailing slashes in module targets are not suppoorted.") 274 target = strings.TrimSuffix(target, string(filepath.Separator)) 275 } 276 return target 277 } 278 279 // TODO: implement this generically in package graph (Bower also has an 280 // implementation) 281 func recurseDeps(pkgMap map[pkg.ID]pkg.Package, p npm.Output) { 282 for name, dep := range p.Dependencies { 283 // Construct ID. 284 id := pkg.ID{ 285 Type: pkg.NodeJS, 286 Name: name, 287 Revision: dep.Version, 288 Location: dep.Resolved, 289 } 290 // Handle previously seen (usually deduplicated) entries: see #257. 291 previous := pkgMap[id] 292 293 // Set direct imports. 294 var imports []pkg.Import 295 for name, i := range dep.Dependencies { 296 imports = append(imports, pkg.Import{ 297 Target: i.From, 298 Resolved: pkg.ID{ 299 Type: pkg.NodeJS, 300 Name: name, 301 Revision: i.Version, 302 Location: i.Resolved, 303 }, 304 }) 305 } 306 // Update map. 307 // NOTE: We're assuming that each deduplicated dependency's imports will 308 // only be listed once. This assumption might not be true. If it's not, then 309 // we need to do a set union instead of a list concatenation. 310 pkgMap[id] = pkg.Package{ 311 ID: id, 312 Imports: append(imports, previous.Imports...), 313 } 314 // Recurse in imports. 315 recurseDeps(pkgMap, dep) 316 } 317 }