pivotal/LicenseFinder

View on GitHub
lib/license_finder/package_managers/yarn.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

module LicenseFinder
  class Yarn < PackageManager
    def initialize(options = {})
      super
      @yarn_options = options[:yarn_options]
    end

    SHELL_COMMAND = 'yarn licenses list --recursive --json'

    def possible_package_paths
      [project_path.join('yarn.lock')]
    end

    def current_packages
      # the licenses plugin supports the classic production flag
      cmd = "#{Yarn::SHELL_COMMAND}#{classic_yarn_production_flag}"
      if yarn_version == 1
        cmd += ' --no-progress'
        cmd += " --cwd #{project_path}" unless project_path.nil?
        cmd += " #{@yarn_options}" unless @yarn_options.nil?
      end

      stdout, stderr, status = Dir.chdir(project_path) { Cmd.run(cmd) }
      raise "Command '#{cmd}' failed to execute: #{stderr}" unless status.success?

      json_strings = stdout.encode('ASCII', invalid: :replace, undef: :replace, replace: '?').split("\n")
      json_objects = json_strings.map { |json_object| JSON.parse(json_object) }

      if yarn_version == 1
        get_yarn1_packages(json_objects)
      else
        get_yarn_packages(json_objects)
      end
    end

    def prepare
      prep_cmd = prepare_command.to_s
      _stdout, stderr, status = Dir.chdir(project_path) { Cmd.run(prep_cmd) }
      return if status.success?

      log_errors stderr
      raise "Prepare command '#{prep_cmd}' failed" unless @prepare_no_fail
    end

    def self.takes_priority_over
      NPM
    end

    def package_management_command
      'yarn'
    end

    def prepare_command
      if yarn_version == 1
        classic_yarn_prepare_command
      else
        yarn_prepare_command
      end
    end

    private

    def yarn_prepare_command
      "#{yarn_plugin_production_command}yarn install && yarn plugin import https://raw.githubusercontent.com/mhassan1/yarn-plugin-licenses/#{yarn_licenses_plugin_version}/bundles/@yarnpkg/plugin-licenses.js"
    end

    def classic_yarn_prepare_command
      "yarn install --ignore-engines --ignore-scripts#{classic_yarn_production_flag}"
    end

    def yarn_licenses_plugin_version
      if yarn_version == 2
        'v0.6.0'
      else
        'v0.7.2'
      end
    end

    def yarn_version
      Dir.chdir(project_path) do
        version_string, stderr_str, status = Dir.chdir(project_path) { Cmd.run('yarn -v') }
        raise "Command 'yarn -v' failed to execute: #{stderr_str}" unless status.success?

        version = version_string.split('.').map(&:to_i)
        return version[0]
      end
    end

    def get_yarn_packages(json_objects)
      packages = []
      incompatible_packages = []
      json_objects.each do |json_object|
        license = json_object['value']
        body = json_object['children']

        body.each do |package_name, vendor_info|
          valid_match = %r{(?<name>@?[\w/.-]+)@(?<manager>\D*):\D*(?<version>(\d+\.?)+)} =~ package_name.to_s
          valid_match = %r{(?<name>@?[\w/.-]+)@virtual:.+#(\D*):\D*(?<version>(\d+\.?)+)} =~ package_name.to_s if manager.eql?('virtual')

          if valid_match
            homepage = vendor_info['children']['vendorUrl']
            author = vendor_info['children']['vendorName']
            package = YarnPackage.new(
              name,
              version,
              spec_licenses: [license],
              homepage: homepage,
              authors: author,
              install_path: project_path.join(modules_folder, name)
            )
            packages << package
          end

          incompatible_match = %r{(?<name>@?[\w/.-]+)@[a-z]*:(?<version>(\.))} =~ package_name.to_s
          if incompatible_match
            package = YarnPackage.new(name, version, spec_licenses: [license])
            incompatible_packages.push(package)
          end
        end
      end

      packages + incompatible_packages.uniq
    end

    def get_yarn1_packages(json_objects)
      packages = []
      if json_objects.last['type'] == 'table'
        license_json = json_objects.pop['data']
        packages = packages_from_json(license_json)
      end

      incompatible_packages = []
      json_objects.each do |json_object|
        match = %r{(?<name>@?[\w/.-]+)@(?<version>(\d+\.?)+)} =~ json_object['data'].to_s
        if match
          package = YarnPackage.new(name, version, spec_licenses: ['unknown'])
          incompatible_packages.push(package)
        end
      end

      packages + incompatible_packages.uniq
    end

    def packages_from_json(json_data)
      body = json_data['body']
      head = json_data['head']

      packages = body.map do |json_package|
        Hash[head.zip(json_package)]
      end

      valid_packages = filter_yarn_internal_package(packages)

      valid_packages.map do |package_hash|
        YarnPackage.new(
          package_hash['Name'],
          package_hash['Version'],
          spec_licenses: [package_hash['License']],
          homepage: package_hash['VendorUrl'],
          authors: package_hash['VendorName'],
          install_path: project_path.join(modules_folder, package_hash['Name'])
        )
      end
    end

    def modules_folder
      return @modules_folder if @modules_folder

      stdout, _stderr, status = Dir.chdir(project_path) { Cmd.run('yarn config get modules-folder') }
      @modules_folder = 'node_modules' if !status.success? || stdout.strip == 'undefined'
      @modules_folder ||= stdout.strip
    end

    # remove fake package created by yarn [Yarn Bug]
    def filter_yarn_internal_package(all_packages)
      internal_package_pattern = /workspace-aggregator-[a-zA-z0-9]{8}-[a-zA-z0-9]{4}-[a-zA-z0-9]{4}-[a-zA-z0-9]{4}-[a-zA-z0-9]{12}/
      yarn_internal_package = all_packages.find { |package| internal_package_pattern.match(package['Name']) }
      all_packages - [yarn_internal_package]
    end

    def classic_yarn_production_flag
      return '' if @ignored_groups.nil?

      @ignored_groups.include?('devDependencies') ? ' --production' : ''
    end

    def yarn_plugin_production_command
      return '' if @ignored_groups.nil?

      @ignored_groups.include?('devDependencies') ? 'yarn plugin import workspace-tools && yarn workspaces focus --all --production && ' : ''
    end
  end
end