antongolub/yarn-audit-fix

View on GitHub
src/main/ts/lockfile/v2.ts

Summary

Maintainability
A
2 hrs
Test Coverage
B
87%
import {
  TAuditReport,
  TFlags,
  TLockfileEntry,
  TLockfileObject,
} from '../ifaces'
import { addHiddenProp, formatFlags, formatYaml, invoke, mapFlags, parseYaml, sortObject } from '../util'

export const parse = (raw: string): TLockfileObject => {
  const data = parseYaml(raw)
  const {__metadata} = data
  delete data.__metadata

  return Object.entries(data).reduce<Record<string, any>>(
    (m, [key, value]: [string, any]) => {
      key.split(', ').forEach((k) => {
        m[k] = value
      })
      return m
    },
    addHiddenProp({}, '__metadata', __metadata),
  )
}

export const patchEntry = (
  entry: TLockfileEntry,
  name: string,
  newVersion: string,
  npmBin: string,
): TLockfileEntry => {
  entry.version = newVersion
  entry.resolution = `${name}@npm:${newVersion}`

  // NOTE seems like deps are not updated by `yarn mode='--update-lockfile'`, only checksums
  entry.dependencies =
    sortObject(JSON.parse(
      invoke(
        npmBin,
        ['view', `${name}@${newVersion}`, 'dependencies', '--json'],
        process.cwd(),
        true,
        false,
      ) || 'null',
    ) || undefined)

  delete entry.checksum

  return entry
}

export const format = (lockfile: TLockfileObject): string => {
  const keymap = Object.entries(lockfile).reduce<Record<string, any>>(
    (m, [k, { resolution }]) => {
      const entry = m[resolution] || (m[resolution] = [])
      entry.push(k)

      return m
    },
    {},
  )

  const data = Object.values(lockfile).reduce<Record<string, any>>(
    (m, value) => {
      const key = keymap[value.resolution].join(', ')
      m[key] = value

      return m
    },
    {
      __metadata: lockfile.__metadata || {
        version: 5,
        cacheKey: 8,
      },
    },
  )

  return `# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

${formatYaml(data, {
  quotingType: '"',
  flowLevel: -1,
  lineWidth: -1,
})
  .replace(/\n([^\s"].+):\n/g, '\n"$1":\n')
  .replace(/\n(\S)/g, '\n\n$1')
  .replace(/resolution: ([^\n"]+)/g, 'resolution: "$1"')}`
}

export const audit = (
  flags: TFlags,
  temp: string,
  bins: Record<string, string>,
): TAuditReport => {
  const mapping = {
    'audit-level': 'severity',
    level: 'severity',
    groups: {
      key: 'environment',
      values: {
        dependencies: 'production',
      },
    },
    only: {
      key: 'environment',
      values: {
        prod: 'production',
      },
    },
  }
  const _flags = formatFlags(
    mapFlags(flags, mapping),
    'exclude',
    'ignore',
    'groups',
    'verbose',
  )
  const report = invoke(
    bins.yarn,
    ['npm', 'audit', '--all', '--json', '--recursive', ..._flags],
    temp,
    !!flags.silent,
    false,
    false,
  )

  return parseAuditReport(report)
}

export const parseAuditReport = (data: string): TAuditReport =>
  Object.values(JSON.parse(data).advisories).reduce<TAuditReport>(
    (m, { vulnerable_versions, module_name, patched_versions }: any) => {
      m[module_name] = {
        patched_versions,
        vulnerable_versions,
        module_name,
      }
      return m
    },
    {},
  )