require 'json' module LicenseFinder class NPM < PackageManager DEPENDENCY_GROUPS = ["dependencies", "devDependencies"] def current_packages packages = {} direct_dependencies.each do |dep| group_name = dep[:group] walk_dependency_tree(dep[:name]) do |dependency| package_id = dependency["name"] if packages[package_id] && packages[package_id].version.nil? && dependency["version"] old_package = packages[package_id] packages[package_id] = NpmPackage.new(dependency, logger: logger, groups: old_package.groups) else packages[package_id] ||= NpmPackage.new(dependency, logger: logger) end packages[package_id].groups << group_name unless packages[package_id].groups.include?(group_name) end end packages.values end def self.package_management_command "npm" end private def direct_dependencies package_json = JSON.parse(File.read(package_path)) DEPENDENCY_GROUPS.map do |group| package_json.fetch(group, {}).keys.map do |dependency| { group: group, name: dependency } end end.flatten end def walk_dependency_tree(dependency, &block) @json ||= npm_json deps = @json.fetch("dependencies", {}).reject { |_,d| d.is_a?(String) } current_dep = deps[dependency] block.call(current_dep) if current_dep recursive_dependencies(current_dep) do |d| block.call(d) end end def npm_json command = 'npm list --json --long' output, success = Dir.chdir(project_path) { capture(command) } if success json = JSON(output) else json = begin JSON(output) rescue JSON::ParserError nil end if json $stderr.puts "Command '#{command}' returned an error but parsing succeeded." else raise "Command '#{command}' failed to execute: #{output}" end end json end def package_path project_path.join('package.json') end def recursive_dependencies(node_module, &block) return unless node_module # node_module can be empty hash if it is included elsewhere block.call(node_module) node_module.fetch('dependencies', {}).each do |dep_key, data| data['name'] ||= dep_key recursive_dependencies(data, &block) end end end end