renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
270 lines (269 loc) • 11.6 kB
JavaScript
import { SYSTEM_INSUFFICIENT_DISK_SPACE } from "../../../../constants/error-messages.js";
import { GlobalConfig } from "../../../../config/global.js";
import { minimatch } from "../../../../util/minimatch.js";
import { logger } from "../../../../logger/index.js";
import { toMs } from "../../../../util/pretty-time.js";
import { trimSlashes } from "../../../../util/url.js";
import { deleteLocalFile, localPathExists, readLocalFile, renameLocalFile } from "../../../../util/fs/index.js";
import { Result } from "../../../../util/result.js";
import { exec, getToolSettingsOptions } from "../../../../util/exec/index.js";
import { getNodeToolConstraint } from "./node-version.js";
import { PackageLock } from "../schema.js";
import { composeLockFile, parseLockFile } from "../utils.js";
import { getNodeOptions, getPackageManagerVersion, lazyLoadPackageJson } from "./utils.js";
import { isNonEmptyString, isNumber, isString } from "@sindresorhus/is";
import { DateTime } from "luxon";
import { quote } from "shlex";
import upath from "upath";
import ini from "ini";
import semver from "semver";
//#region lib/modules/manager/npm/post-update/npm.ts
function parseNpmrcCooldownDate(npmrcContent) {
if (!npmrcContent) return null;
const parsed = ini.parse(npmrcContent);
const before = parsed.before;
if (isNonEmptyString(before)) {
const dt = DateTime.fromISO(before, { zone: "utc" });
if (dt.isValid) return {
date: dt,
source: "before"
};
logger.debug(`Invalid before date in .npmrc: ${before}, ignoring`);
}
const minReleaseAge = parsed["min-release-age"];
if (isNonEmptyString(minReleaseAge)) {
const days = parseInt(minReleaseAge, 10);
if (isNumber(days) && days >= 0) return {
date: DateTime.now().minus({ days }).toUTC(),
source: "min-release-age"
};
logger.debug(`Invalid min-release-age in .npmrc: ${minReleaseAge}, ignoring`);
}
return null;
}
async function getNpmConstraintFromPackageLock(lockFileDir, filename) {
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`;
}
return null;
}
async function generateLockFile(lockFileDir, env, filename, config = {}, upgrades = [], npmrcContent = null) {
const lockFileName = upath.join(lockFileDir, filename);
logger.debug(`Spawning npm install to create ${lockFileDir}/${filename}`);
const { skipInstalls, postUpdateOptions } = config;
let lockFile = null;
let beforeFallback = false;
try {
const lazyPkgJson = lazyLoadPackageJson(lockFileDir);
const npmToolConstraint = {
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);
let commands = [];
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";
let beforeFlag = "";
if (config.minimumReleaseAge) {
const ms = toMs(config.minimumReleaseAge);
if (ms === null) logger.debug({ minimumReleaseAge: config.minimumReleaseAge }, "Invalid minimumReleaseAge, skipping --before for npm install");
else {
const npmrcCooldown = parseNpmrcCooldownDate(npmrcContent);
if (npmrcCooldown?.source === "min-release-age") logger.debug({
npmrcMinReleaseAge: npmrcCooldown.date.toISO(),
minimumReleaseAge: config.minimumReleaseAge
}, "Skipping --before flag because .npmrc already contains min-release-age");
else {
let beforeDate = DateTime.now().minus(ms).toUTC();
if (npmrcCooldown && npmrcCooldown.date < beforeDate) {
logger.debug({
npmrcDate: npmrcCooldown.date.toISO(),
beforeDate: beforeDate.toISO()
}, "Using stricter .npmrc cooldown date over minimumReleaseAge date");
beforeDate = npmrcCooldown.date;
}
const beforeISO = beforeDate.toISO();
logger.debug({
beforeISO,
minimumReleaseAge: config.minimumReleaseAge
}, "Setting npm --before based on minimumReleaseAge");
beforeFlag = ` --before=${beforeISO}`;
}
}
}
const extraEnv = {
NPM_CONFIG_CACHE: env.NPM_CONFIG_CACHE,
npm_config_store: env.npm_config_store
};
const { nodeMaxMemory } = getToolSettingsOptions(config.toolSettings);
if (nodeMaxMemory) extraEnv.NODE_OPTIONS = getNodeOptions(nodeMaxMemory);
const execOptions = {
cwdFile: lockFileName,
extraEnv,
toolConstraints: [await getNodeToolConstraint(config, upgrades, lockFileDir, lazyPkgJson), ...isNonEmptyString(npmToolConstraint.constraint) ? [npmToolConstraint] : []],
docker: {}
};
/* v8 ignore next 4 -- needs test */
if (GlobalConfig.get("exposeAllEnv")) {
extraEnv.NPM_AUTH = env.NPM_AUTH;
extraEnv.NPM_EMAIL = env.NPM_EMAIL;
}
if (!upgrades.every((upgrade) => upgrade.isLockfileUpdate)) commands.push(`npm install ${cmdOptions}${beforeFlag}`.trim());
const lockUpdates = upgrades.filter((upgrade) => upgrade.isLockfileUpdate);
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));
// v8 ignore else -- TODO: add test #40625
if (currentWorkspaceUpdates.length) {
const updateCmd = `npm install ${cmdOptions}${beforeFlag} --workspace=${quote(workspace)} ${currentWorkspaceUpdates.map(quote).join(" ")}`;
commands.push(updateCmd);
}
}
}
if (lockRootUpdates.length) {
logger.debug("Performing lockfileUpdate (npm)");
const updateCmd = `npm install ${cmdOptions}${beforeFlag} ${lockRootUpdates.map((update) => update.managerData?.packageKey).map(quote).join(" ")}`;
commands.push(updateCmd);
}
if (upgrades.some((upgrade) => upgrade.isRemediation)) commands.push(`npm install ${cmdOptions}${beforeFlag}`.trim());
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) /* v8 ignore next -- TODO: add test #40625 */ {
logger.debug({
err,
lockFileName
}, "Error removing `package-lock.json` for lock file maintenance");
}
}
if (postUpdateOptions?.includes("npmInstallTwice")) {
logger.debug("Running npm install twice");
const existingCommands = [...commands];
commands = [];
for (const command of existingCommands) {
commands.push(command);
// v8 ignore else -- TODO: add test #40625
if (command.startsWith("npm install")) commands.push(command);
}
}
await exec(commands, execOptions).catch(async (err) => {
if (beforeFlag && err.stderr?.includes("with a date before")) {
logger.debug("npm --before caused ETARGET, retrying without --before");
const commandsWithoutBefore = commands.map((cmd) => cmd.replace(beforeFlag, ""));
beforeFallback = true;
return exec(commandsWithoutBefore, execOptions);
}
throw err;
});
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"));
lockFile = await readLocalFile(upath.join(lockFileDir, filename), "utf8");
if (lockUpdates.length) {
const { detectedIndent, lockFileParsed } = parseLockFile(lockFile);
if (lockFileParsed?.lockfileVersion === 2 || lockFileParsed?.lockfileVersion === 3) {
lockUpdates.forEach((lockUpdate) => {
const depType = lockUpdate.depType;
// v8 ignore else -- TODO: add test #40625
if (lockFileParsed.packages?.[""]?.[depType]?.[lockUpdate.packageName]) lockFileParsed.packages[""][depType][lockUpdate.packageName] = lockUpdate.newValue;
});
lockFile = composeLockFile(lockFileParsed, detectedIndent);
}
}
} catch (err) {
// v8 ignore if -- TODO: add test #40625
if (err.message === "temporary-error") throw err;
logger.debug({
err,
type: "npm"
}, "lock file error");
// v8 ignore if -- TODO: add test #40625
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,
beforeFallback
};
}
function divideWorkspaceAndRootDeps(lockFileDir, lockUpdates) {
const lockRootUpdates = [];
const lockWorkspacesUpdates = [];
const workspaces = /* @__PURE__ */ new Set();
const rootDeps = /* @__PURE__ */ new Set();
for (const upgrade of lockUpdates) {
upgrade.managerData ??= {};
upgrade.managerData.packageKey = generatePackageKey(upgrade.packageName, upgrade.newVersion);
if (upgrade.managerData.workspacesPackages?.length && isString(upgrade.packageFile)) {
const workspacePatterns = upgrade.managerData.workspacesPackages;
const packageFileDir = trimSlashes(upgrade.packageFile.replace("package.json", ""));
const workspaceDir = trimSlashes(packageFileDir.startsWith(lockFileDir) ? packageFileDir.slice(lockFileDir.length) : packageFileDir);
if (isNonEmptyString(workspaceDir)) {
let workspaceName;
for (const workspacePattern of workspacePatterns) if (minimatch(workspacePattern.replace(/^\.\//, "")).match(workspaceDir)) {
workspaceName = workspaceDir;
break;
}
if (workspaceName) {
// v8 ignore else -- TODO: add test #40625
if (!rootDeps.has(upgrade.managerData.packageKey)) {
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, version) {
return `${packageName}@${version}`;
}
//#endregion
export { generateLockFile };
//# sourceMappingURL=npm.js.map