loading...
Cover image for Yarn audit fix: workaround

Yarn audit fix: workaround

antongolub profile image Anton Golub Updated on ・3 min read

tl;dr

npx yarn-audit-fix

Issue

Auditing and updating dependencies is an important part of the code lifecycle. There are different approaches to implement this task: services (renovate, snyk, dependabot and so on) and instruments (npm, yarn).

If you develop proprietary things in an isolated environment, external services are most likely unavailable. If you use monorepos, yarn is your only choice.

But there is a problem.

yarn audit fix

...does not quite what is expected. It just shows vulnerabilities, but doesn't fix them. And here is a long discussion of why this happens: https://github.com/yarnpkg/yarn/issues/7075

Most workarounds are based on different variations of semi-manual dependency patching.

{
  "resolutions": {
    "package-merge-lodash-4/*/lodash": "4.17.12"
  },
}
...
"scripts": {
  "preinstall": "npx npm-force-resolutions"
}

Fortunately, Gianfranco P. proposed another solution:

1. npm i --package-lock-only
2. npm audit fix
3. rm yarn.lock
4. yarn import
5. rm package-lock.json

It's simple, clear and it works. But this script ignores the case of monorepos, because npm doesn't support workspaces yet.

There must be another way to generate lock file. It exists! Synp converts yarn.lock to package-lock.json and vice versa. So

synp --source-file yarn.lock

gives package-lock.json, that contains all the dependencies (nope, but it will be fixed asap. Fixed) of the monorepository. Next npm audit fix works normally.
However, yarn import raises an error.

error An unexpected error occurred: "https://registry.yarnpkg.com/workspace-aggregator-bbb13589-b149-4858-b202-4f4e90c33e3f: Not found".
info If you think this is a bug, please open a bug report with the information provided in "/<root>/projects/foobar/yarn-error.log".
info Visit https://yarnpkg.com/en/docs/cli/import for documentation about this command.

One more side-effect of workspace field in package.json. Not a problem. It disappears if you temporarily remove/rename the field before synp invocation.

Performing these steps manually each time is slightly tedious. Let's automate.

import fs from 'fs-extra'
import synp from 'synp'
import {join} from 'path'
import findCacheDir from 'find-cache-dir'
import chalk from 'chalk'
import {invoke} from './invoke'

type TContext = { cwd: string, temp: string }

type TCallback = (cxt: TContext) => void

type TStage = [string, ...TCallback[]]

/**
 * Prepare temp assets.
 * @param {TContext} cxt
 * @return {void}
 */
const createTempAssets: TCallback = ({temp}) => {
  fs.copyFileSync('yarn.lock', join(temp, 'yarn.lock'))
  fs.copyFileSync('package.json', join(temp, 'package.json'))
  fs.createSymlinkSync('node_modules', join(temp, 'node_modules'), 'dir')
}

/**
 * Remove workspace field from package.json due to npm issue.
 * https://github.com/antongolub/yarn-audit-fix/issues/2
 * @param {TContext} cxt
 * @return {void}
 */
const fixWorkspaces: TCallback = ({temp}) => {
  const pkgJsonData = JSON.parse(fs.readFileSync(join(temp, 'package.json'), 'utf-8').trim())
  delete pkgJsonData.workspaces

  fs.writeFileSync(join(temp, 'package.json'), JSON.stringify(pkgJsonData, null, 2))
}

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

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

/**
 * Apply npm audit fix.
 * @param {TContext} cxt
 * @return {void}
 */
const npmAuditFix: TCallback = ({temp}) =>
  invoke('npm', ['audit', 'fix', '--package-lock-only'], temp)

/**
 * Generate yarn.lock by package-lock.json data.
 * @param {TContext} cxt
 * @return {void}
 */
const yarnImport: TCallback = ({temp}) => {
  invoke('yarn', ['import'], temp)
  fs.copyFileSync(join(temp, 'yarn.lock'), 'yarn.lock')
}

/**
 * Apply yarn install to fetch packages after yarn.lock update.
 * @param {TContext} cxt
 * @return {void}
 */
const yarnInstall: TCallback = ({cwd}) =>
  invoke('yarn', [], cwd)

/**
 * Clean up temporaries.
 * @param {TContext} cxt
 * @return {void}
 */
const clear: TCallback = ({temp}) =>
  fs.emptyDirSync(temp)

export const stages: TStage[] = [
  [
    'Preparing temp assets...',
    clear,
    createTempAssets,
    fixWorkspaces,
  ],
  [
    'Generating package-lock.json from yarn.lock...',
    yarnLockToPkgLock,
  ],
  [
    'Applying npm audit fix...',
    npmAuditFix,
  ],
  [
    'Updating yarn.lock from package-lock.json...',
    yarnImport,
    yarnInstall,
    clear,
  ],
  [
    'Done',
  ],
]

/**
 * Public static void main.
 */
export const run = async() => {
  const ctx = {
    cwd: process.cwd(),
    temp: findCacheDir({name: 'yarn-audit-fix', create: true}) + '',
  }

  for (const [description, ...steps] of stages) {
    console.log(chalk.bold(description))

    for (const step of steps) step(ctx)
  }
}

And finally it looks to be working.

https://github.com/antongolub/yarn-audit-fix
Any feedback is welcome.

Posted on by:

Discussion

markdown guide
 

Very nice tool, my suggestion would be to create a custom script in package.json:

"audit:fix": "npx yarn-audit-fix",

And of course yarn-audit-fix as devDependency.

So it's even easier to run audit fix:

yarn audit:fix