antongolub/yarn-audit-fix

View on GitHub
src/main/ts/stages.ts

Summary

Maintainability
A
35 mins
Test Coverage
A
94%
import { dirname, join, relative } from 'node:path'

import fs from 'fs-extra'
import semver from 'semver'
import synp from 'synp'

import { TCallback } from './ifaces'
import * as lf from './lockfile'
import { format, getLockfileType } from './lockfile'
import {
  formatFlags,
  getBinVersion,
  getNpm,
  getSelfManifest,
  getSymlinkType,
  getWorkspaces,
  getYarn,
  invoke,
} from './util'


/**
 * Resolve bins.
 */
export const resolveBins: TCallback = ({ ctx, flags }) => {
  const yafManifest = getSelfManifest()
  ctx.bins = {
    yarn: getYarn(),
    npm: getNpm(flags['npm-path']),
  }
  ctx.versions = {
    node: getBinVersion('node'),
    npm: getBinVersion(ctx.bins.npm),
    yarn: getBinVersion(ctx.bins.yarn),
    yaf: yafManifest.version,
    yafLatest: invoke(
      ctx.bins.npm,
      ['view', yafManifest.name, 'version'],
      process.cwd(),
      true,
      false,
    ) as string,
  }
}

/**
 * Print runtime context digest.
 */
export const printRuntimeDigest: TCallback = ({
  temp,
  cwd,
  flags,
  bins,
  versions,
  manifest,
}) => {
  if (flags.silent) {
    return
  }
  const isMonorepo = !!manifest.workspaces
  // NOTE npm > 7.0.0 provides monorepo support
  if (
    isMonorepo &&
    (semver.parse(versions.npm as string)?.major as number) < 7
  ) {
    console.warn(
      "This project looks like monorepo, so it's recommended to use `npm v7+` to process workspaces",
    )
  }

  // NOTE yarn > v3.3.0 fixed plugin-npm-cli minor compatibility
  // https://github.com/yarnpkg/berry/pull/4356#issuecomment-1316653931
  if (semver.gt('3.3.0', versions.yarn) && (flags.exclude || flags.ignore)) {
    console.warn(
      `This project yarn version ${versions.yarn} doesn't support the 'exclude' and 'ignore' flags. Please upgrade to yarn 3.3.0 or higher to use those flags`,
    )
  }

  if (semver.gt(versions.yafLatest, versions.yaf)) {
    console.warn(
      `yarn-audit-fix version ${versions.yaf} is out of date. Install the latest ${versions.yafLatest} for better results`,
    )
  }

  console.log(
    JSON.stringify(
      {
        isMonorepo,
        bins,
        versions,
        temp,
        cwd,
        flags,
      },
      undefined,
      2,
    ).replace(/[",:{}]/g, ''),
  )
}

/**
 * Prepare temp assets.
 * @param {TContext} cxt
 * @return {void}
 */
export const createTempAssets: TCallback = ({ cwd, temp }) => {
  fs.copyFileSync(join(cwd, 'yarn.lock'), join(temp, 'yarn.lock'))
  fs.copyFileSync(join(cwd, 'package.json'), join(temp, 'package.json'))
  fs.existsSync(join(cwd, '.npmrc')) &&
    fs.copyFileSync(join(cwd, '.npmrc'), join(temp, '.npmrc'))
  fs.existsSync(join(cwd, '.yarnrc')) &&
    fs.copyFileSync(join(cwd, '.yarnrc'), join(temp, '.yarnrc'))
}

/**
 * Provide symlinks to node_modules and workspaces
 * @param {TContext} cxt
 * @return {void}
 */
export const createSymlinks: TCallback = ({ temp, flags, cwd, manifest }) => {
  const symlinkType = getSymlinkType(flags.symlink)
  const workspaces = getWorkspaces(cwd, manifest)
  const links = [
    join(cwd, 'node_modules'),
    join(cwd, '.yarn'),
    ...workspaces.map((ws) => dirname(ws)),
  ]

  links.forEach((link: string) => {
    const rel = relative(cwd, link)
    const from = join(cwd, rel)
    const to = join(temp, rel)

    fs.existsSync(from) && fs.createSymlinkSync(from, to, symlinkType)
  })
}

/**
 * Convert yarn.lock to package-lock.json for further audit.
 * @param {TContext} cxt
 * @return {void}
 */
export const yarnLockToPkgLock: TCallback = ({ temp, flags }) => {
  const pgkLockJsonData = synp.yarnToNpm(temp, true)

  fs.writeFileSync(join(temp, 'package-lock.json'), pgkLockJsonData)

  if (flags.flow !== 'patch') {
    fs.removeSync(join(temp, 'yarn.lock'))
  }
}

/**
 * Apply npm audit fix.
 * @param {TContext} cxt
 * @return {void}
 */
export const npmAuditFix: TCallback = ({ temp, flags, bins }) => {
  const defaultFlags = {
    'package-lock-only': true,
  }
  const auditFlags = formatFlags(
    { ...defaultFlags, ...flags },
    'audit-level',
    'dry-run',
    'exclude',
    'force',
    'ignore',
    'loglevel',
    'legacy-peer-deps',
    'only',
    'package-lock-only',
    'registry',
    'silent',
    'verbose',
  )
  const auditArgs = ['audit', 'fix', ...auditFlags, '--prefix', temp]

  invoke(bins.npm, auditArgs, temp, flags.silent)
}

/**
 * Generate yarn.lock by package-lock.json data.
 * @param {TContext} cxt
 * @return {void}
 */
export const yarnImport: TCallback = ({ temp }) => {
  const yarnLockData = synp.npmToYarn(temp, true)

  fs.writeFileSync(join(temp, 'yarn.lock'), yarnLockData)
}

export const syncLockfile: TCallback = ({ temp, flags }) => {
  if (flags.dryRun) {
    return
  }

  fs.copyFileSync(join(temp, 'yarn.lock'), 'yarn.lock')
}

/**
 * Apply yarn install to fetch packages after yarn.lock update.
 * @param {TContext} cxt
 * @return {void}
 */
export const yarnInstall: TCallback = ({ cwd, flags, versions, bins }) => {
  if (flags.dryRun) {
    return
  }

  semver.gte(versions.yarn, '2.0.0')
    ? invoke(
        bins.yarn,
        ['install', '--mode=update-lockfile'],
        cwd,
        flags.silent,
      )
    : invoke(
        bins.yarn,
        [
          'install',
          '--update-checksums',
          ...formatFlags(
            flags,
            'verbose',
            'silent',
            'registry',
            'ignore-engines',
          ),
        ],
        cwd,
        flags.silent,
      )
}
/**
 * Clean up temporaries.
 * @param {TContext} cxt
 * @return {void}
 */
export const clear: TCallback = ({ temp }) => fs.emptyDirSync(temp)

/**
 * Exit on error.
 * @param {TContext} cxt
 * @return {void}
 */
export const exit: TCallback = ({ flags, err }) => {
  !flags.silent && console.error(err)
  process.exitCode = err?.status | 0 || 1
}

export const patchLockfile: TCallback = ({ temp, ctx }) => {
  const lockfilePath = join(temp, 'yarn.lock')
  const raw = fs.readFileSync(lockfilePath, 'utf-8')
  const lockfileType = getLockfileType(raw)
  const lockfile = lf.parse(raw, lockfileType)
  const report = lf.audit(ctx, lockfileType)
  const patched = lf.patch(lockfile, report, ctx, lockfileType)

  fs.writeFileSync(lockfilePath, format(patched, lockfileType))
}

/**
 * Check that everything is fine with pkg dir.
 * @param {TContext} cxt
 * @return {void}
 */
export const verify: TCallback = ({ cwd, versions, flags }) => {
  const required = ['yarn.lock', 'package.json']

  // NOTE yarn 2+ in PnP mode does not create `node_modules` dir
  if (flags.flow === 'convert' || semver.lt(versions.yarn, '2.0.0')) {
    required.push('node_modules')
  }

  required.forEach((resource) => {
    if (!fs.existsSync(join(cwd, resource))) {
      throw new Error(`not found: ${resource}`)
    }
  })
}