mirror of https://github.com/renovatebot/renovate
342 lines
11 KiB
TypeScript
342 lines
11 KiB
TypeScript
// TODO: types (#22198)
|
|
import is from '@sindresorhus/is';
|
|
import semver from 'semver';
|
|
import upath from 'upath';
|
|
import { GlobalConfig } from '../../../../config/global';
|
|
import {
|
|
SYSTEM_INSUFFICIENT_DISK_SPACE,
|
|
TEMPORARY_ERROR,
|
|
} from '../../../../constants/error-messages';
|
|
import { logger } from '../../../../logger';
|
|
import { exec } from '../../../../util/exec';
|
|
import type {
|
|
ExecOptions,
|
|
ExtraEnv,
|
|
ToolConstraint,
|
|
} from '../../../../util/exec/types';
|
|
import {
|
|
deleteLocalFile,
|
|
localPathExists,
|
|
readLocalFile,
|
|
renameLocalFile,
|
|
} from '../../../../util/fs';
|
|
import { minimatch } from '../../../../util/minimatch';
|
|
import { Result } from '../../../../util/result';
|
|
import { trimSlashes } from '../../../../util/url';
|
|
import type { PostUpdateConfig, Upgrade } from '../../types';
|
|
import { PackageLock } from '../schema';
|
|
import { composeLockFile, parseLockFile } from '../utils';
|
|
import { getNodeToolConstraint } from './node-version';
|
|
import type { GenerateLockFileResult } from './types';
|
|
import { getPackageManagerVersion, lazyLoadPackageJson } from './utils';
|
|
|
|
async function getNpmConstraintFromPackageLock(
|
|
lockFileDir: string,
|
|
filename: string,
|
|
): Promise<string | null> {
|
|
const packageLockFileName = upath.join(lockFileDir, filename);
|
|
const packageLockContents = await readLocalFile(packageLockFileName, 'utf8');
|
|
const packageLockJson = Result.parse(
|
|
packageLockContents,
|
|
PackageLock,
|
|
).unwrapOrNull();
|
|
if (!packageLockJson) {
|
|
logger.debug(`Could not parse ${packageLockFileName}`);
|
|
return null;
|
|
}
|
|
const { lockfileVersion } = packageLockJson;
|
|
if (lockfileVersion === 1) {
|
|
logger.debug(`Using npm constraint <7 for lockfileVersion=1`);
|
|
return `<7`;
|
|
}
|
|
if (lockfileVersion === 2) {
|
|
logger.debug(`Using npm constraint <9 for lockfileVersion=2`);
|
|
return `<9`;
|
|
}
|
|
logger.debug(
|
|
`Using npm constraint >=9 for lockfileVersion=${lockfileVersion}`,
|
|
);
|
|
return `>=9`;
|
|
}
|
|
|
|
export async function generateLockFile(
|
|
lockFileDir: string,
|
|
env: NodeJS.ProcessEnv,
|
|
filename: string,
|
|
config: Partial<PostUpdateConfig> = {},
|
|
upgrades: Upgrade[] = [],
|
|
): Promise<GenerateLockFileResult> {
|
|
// TODO: don't assume package-lock.json is in the same directory
|
|
const lockFileName = upath.join(lockFileDir, filename);
|
|
|
|
logger.debug(`Spawning npm install to create ${lockFileDir}/${filename}`);
|
|
const { skipInstalls, postUpdateOptions } = config;
|
|
|
|
let lockFile: string | null = null;
|
|
try {
|
|
const lazyPkgJson = lazyLoadPackageJson(lockFileDir);
|
|
const npmToolConstraint: ToolConstraint = {
|
|
toolName: 'npm',
|
|
constraint:
|
|
config.constraints?.npm ??
|
|
getPackageManagerVersion('npm', await lazyPkgJson.getValue()) ??
|
|
(await getNpmConstraintFromPackageLock(lockFileDir, filename)) ??
|
|
null,
|
|
};
|
|
const supportsPreferDedupeFlag =
|
|
!npmToolConstraint.constraint ||
|
|
semver.intersects('>=7.0.0', npmToolConstraint.constraint);
|
|
const commands: string[] = [];
|
|
let cmdOptions = '';
|
|
if (
|
|
(postUpdateOptions?.includes('npmDedupe') === true &&
|
|
!supportsPreferDedupeFlag) ||
|
|
skipInstalls === false
|
|
) {
|
|
logger.debug('Performing node_modules install');
|
|
cmdOptions += '--no-audit';
|
|
} else {
|
|
logger.debug('Updating lock file only');
|
|
cmdOptions += '--package-lock-only --no-audit';
|
|
}
|
|
|
|
if (postUpdateOptions?.includes('npmDedupe') && supportsPreferDedupeFlag) {
|
|
logger.debug('Deduplicate dependencies on installation');
|
|
cmdOptions += ' --prefer-dedupe';
|
|
}
|
|
|
|
if (!GlobalConfig.get('allowScripts') || config.ignoreScripts) {
|
|
cmdOptions += ' --ignore-scripts';
|
|
}
|
|
|
|
const extraEnv: ExtraEnv = {
|
|
NPM_CONFIG_CACHE: env.NPM_CONFIG_CACHE,
|
|
npm_config_store: env.npm_config_store,
|
|
};
|
|
const execOptions: ExecOptions = {
|
|
cwdFile: lockFileName,
|
|
userConfiguredEnv: config.env,
|
|
extraEnv,
|
|
toolConstraints: [
|
|
await getNodeToolConstraint(config, upgrades, lockFileDir, lazyPkgJson),
|
|
npmToolConstraint,
|
|
],
|
|
docker: {},
|
|
};
|
|
// istanbul ignore if
|
|
if (GlobalConfig.get('exposeAllEnv')) {
|
|
extraEnv.NPM_AUTH = env.NPM_AUTH;
|
|
extraEnv.NPM_EMAIL = env.NPM_EMAIL;
|
|
}
|
|
|
|
if (!upgrades.every((upgrade) => upgrade.isLockfileUpdate)) {
|
|
// This command updates the lock file based on package.json
|
|
commands.push(`npm install ${cmdOptions}`.trim());
|
|
}
|
|
|
|
// rangeStrategy = update-lockfile
|
|
const lockUpdates = upgrades.filter((upgrade) => upgrade.isLockfileUpdate);
|
|
|
|
// divide the deps in two categories: workspace and root
|
|
const { lockRootUpdates, lockWorkspacesUpdates, workspaces, rootDeps } =
|
|
divideWorkspaceAndRootDeps(lockFileDir, lockUpdates);
|
|
|
|
if (workspaces.size && lockWorkspacesUpdates.length) {
|
|
logger.debug('Performing lockfileUpdate (npm-workspaces)');
|
|
for (const workspace of workspaces) {
|
|
const currentWorkspaceUpdates = lockWorkspacesUpdates
|
|
.filter((update) => update.workspace === workspace)
|
|
.map((update) => update.managerData?.packageKey)
|
|
.filter((packageKey) => !rootDeps.has(packageKey));
|
|
|
|
if (currentWorkspaceUpdates.length) {
|
|
const updateCmd = `npm install ${cmdOptions} --workspace=${workspace} ${currentWorkspaceUpdates.join(
|
|
' ',
|
|
)}`;
|
|
commands.push(updateCmd);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (lockRootUpdates.length) {
|
|
logger.debug('Performing lockfileUpdate (npm)');
|
|
const updateCmd =
|
|
`npm install ${cmdOptions} ` +
|
|
lockRootUpdates
|
|
.map((update) => update.managerData?.packageKey)
|
|
.join(' ');
|
|
commands.push(updateCmd);
|
|
}
|
|
|
|
if (upgrades.some((upgrade) => upgrade.isRemediation)) {
|
|
// We need to run twice to get the correct lock file
|
|
commands.push(`npm install ${cmdOptions}`.trim());
|
|
}
|
|
|
|
// postUpdateOptions
|
|
if (
|
|
config.postUpdateOptions?.includes('npmDedupe') &&
|
|
!supportsPreferDedupeFlag
|
|
) {
|
|
logger.debug('Performing npm dedupe after installation');
|
|
commands.push('npm dedupe');
|
|
}
|
|
|
|
if (upgrades.find((upgrade) => upgrade.isLockFileMaintenance)) {
|
|
logger.debug(
|
|
`Removing ${lockFileName} first due to lock file maintenance upgrade`,
|
|
);
|
|
try {
|
|
await deleteLocalFile(lockFileName);
|
|
} catch (err) /* istanbul ignore next */ {
|
|
logger.debug(
|
|
{ err, lockFileName },
|
|
'Error removing `package-lock.json` for lock file maintenance',
|
|
);
|
|
}
|
|
}
|
|
|
|
// Run the commands
|
|
await exec(commands, execOptions);
|
|
|
|
// massage to shrinkwrap if necessary
|
|
if (
|
|
filename === 'npm-shrinkwrap.json' &&
|
|
(await localPathExists(upath.join(lockFileDir, 'package-lock.json')))
|
|
) {
|
|
await renameLocalFile(
|
|
upath.join(lockFileDir, 'package-lock.json'),
|
|
upath.join(lockFileDir, 'npm-shrinkwrap.json'),
|
|
);
|
|
}
|
|
|
|
// Read the result
|
|
// TODO #22198
|
|
lockFile = (await readLocalFile(
|
|
upath.join(lockFileDir, filename),
|
|
'utf8',
|
|
))!;
|
|
|
|
// Massage lockfile counterparts of package.json that were modified
|
|
// because npm install was called with an explicit version for rangeStrategy=update-lockfile
|
|
if (lockUpdates.length) {
|
|
const { detectedIndent, lockFileParsed } = parseLockFile(lockFile);
|
|
if (
|
|
lockFileParsed?.lockfileVersion === 2 ||
|
|
lockFileParsed?.lockfileVersion === 3
|
|
) {
|
|
lockUpdates.forEach((lockUpdate) => {
|
|
const depType = lockUpdate.depType as
|
|
| 'dependencies'
|
|
| 'optionalDependencies';
|
|
|
|
// TODO #22198
|
|
if (
|
|
lockFileParsed.packages?.['']?.[depType]?.[lockUpdate.packageName!]
|
|
) {
|
|
lockFileParsed.packages[''][depType][lockUpdate.packageName!] =
|
|
lockUpdate.newValue!;
|
|
}
|
|
});
|
|
lockFile = composeLockFile(lockFileParsed, detectedIndent);
|
|
}
|
|
}
|
|
} catch (err) /* istanbul ignore next */ {
|
|
if (err.message === TEMPORARY_ERROR) {
|
|
throw err;
|
|
}
|
|
logger.debug(
|
|
{
|
|
err,
|
|
type: 'npm',
|
|
},
|
|
'lock file error',
|
|
);
|
|
if (err.stderr?.includes('ENOSPC: no space left on device')) {
|
|
throw new Error(SYSTEM_INSUFFICIENT_DISK_SPACE);
|
|
}
|
|
return { error: true, stderr: err.stderr };
|
|
}
|
|
return { error: !lockFile, lockFile };
|
|
}
|
|
|
|
export function divideWorkspaceAndRootDeps(
|
|
lockFileDir: string,
|
|
lockUpdates: Upgrade[],
|
|
): {
|
|
lockRootUpdates: Upgrade[];
|
|
lockWorkspacesUpdates: Upgrade[];
|
|
workspaces: Set<string>;
|
|
rootDeps: Set<string>;
|
|
} {
|
|
const lockRootUpdates: Upgrade[] = []; // stores all upgrades which are present in root package.json
|
|
const lockWorkspacesUpdates: Upgrade[] = []; // stores all upgrades which are present in workspaces package.json
|
|
const workspaces = new Set<string>(); // name of all workspaces
|
|
const rootDeps = new Set<string>(); // packageName of all upgrades in root package.json (makes it check duplicate deps in root)
|
|
|
|
// divide the deps in two categories: workspace and root
|
|
for (const upgrade of lockUpdates) {
|
|
upgrade.managerData ??= {};
|
|
upgrade.managerData.packageKey = generatePackageKey(
|
|
upgrade.packageName!,
|
|
upgrade.newVersion!,
|
|
);
|
|
if (
|
|
upgrade.managerData.workspacesPackages?.length &&
|
|
is.string(upgrade.packageFile)
|
|
) {
|
|
const workspacePatterns = upgrade.managerData.workspacesPackages; // glob pattern or directory name/path
|
|
const packageFileDir = trimSlashes(
|
|
upgrade.packageFile.replace('package.json', ''),
|
|
);
|
|
|
|
// workspaceDir = packageFileDir - lockFileDir
|
|
const workspaceDir = trimSlashes(
|
|
packageFileDir.startsWith(lockFileDir)
|
|
? packageFileDir.slice(lockFileDir.length)
|
|
: packageFileDir,
|
|
);
|
|
|
|
if (is.nonEmptyString(workspaceDir)) {
|
|
let workspaceName: string | undefined;
|
|
// compare workspaceDir to workspace patterns
|
|
// stop when the first match is found and
|
|
// add workspaceDir to workspaces set and upgrade object
|
|
for (const workspacePattern of workspacePatterns) {
|
|
const massagedPattern = (workspacePattern as string).replace(
|
|
/^\.\//,
|
|
'',
|
|
);
|
|
if (minimatch(massagedPattern).match(workspaceDir)) {
|
|
workspaceName = workspaceDir;
|
|
break;
|
|
}
|
|
}
|
|
if (workspaceName) {
|
|
if (
|
|
!rootDeps.has(upgrade.managerData.packageKey) // prevent same dep from existing in root and workspace
|
|
) {
|
|
workspaces.add(workspaceName);
|
|
upgrade.workspace = workspaceName;
|
|
lockWorkspacesUpdates.push(upgrade);
|
|
}
|
|
} else {
|
|
logger.warn(
|
|
{ workspacePatterns, workspaceDir },
|
|
'workspaceDir not found',
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
lockRootUpdates.push(upgrade);
|
|
rootDeps.add(upgrade.managerData.packageKey);
|
|
}
|
|
|
|
return { lockRootUpdates, lockWorkspacesUpdates, workspaces, rootDeps };
|
|
}
|
|
|
|
function generatePackageKey(packageName: string, version: string): string {
|
|
return `${packageName}@${version}`;
|
|
}
|