UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

270 lines (269 loc) • 11.6 kB
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