Yarn audit fix: workaround
Tags: javascript, yarn, audit, vulnerabilities
Published: 2020-07-10 17:32:11

### 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.
* [how-to-fix-security-vulnerabilities-in-npm-yarn-dependencies/](https://alfilatov.com/posts/how-to-fix-security-vulnerabilities-in-npm-yarn-dependencies)
* [fix-npm-vulnerabilities-with-yarn](https://medium.com/@rsrathaurorai/fix-npm-vulnerabilities-with-yarn-9cddc973d283)
```
{
  "resolutions": {
    "package-merge-lodash-4/*/lodash": "4.17.12"
  },
}
...
"scripts": {
  "preinstall": "npx npm-force-resolutions"
}
```

Fortunately, Gianfranco P. [proposed another solution](https://stackoverflow.com/questions/51732435/is-there-a-yarn-alternative-for-npm-audit/60878037#60878037):
```
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](https://github.com/imsnif/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](https://github.com/imsnif/synp/issues/44)~~. [Fixed](https://github.com/imsnif/synp/pull/45)) 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.
```typescript
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.  
{% github antongolub/yarn-audit-fix no-readme %}
Any feedback is welcome.