src/main/ts/stages.ts
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}`)
}
})
}