diff --git a/lib/manager/npm/post-update/node-version.spec.ts b/lib/manager/npm/post-update/node-version.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..af76ab43ec3a65b6c65575d7b42b48ec2bc9e903 --- /dev/null +++ b/lib/manager/npm/post-update/node-version.spec.ts @@ -0,0 +1,45 @@ +import { mocked } from '../../../../test/util'; +import * as fs_ from '../../../util/fs'; +import { getNodeConstraint } from './node-version'; + +const fs = mocked(fs_); + +describe('getNodeConstraint', () => { + it('returns package.json range', async () => { + fs.readLocalFile = jest.fn(); + fs.readLocalFile.mockResolvedValueOnce(null); + fs.readLocalFile.mockResolvedValueOnce(null); + fs.readLocalFile.mockResolvedValueOnce('{"engines":{"node":"^12.16.0"}}'); + const res = await getNodeConstraint('package.json'); + expect(res).toEqual('^12.16.0'); + }); + it('returns .node-version value', async () => { + fs.readLocalFile = jest.fn(); + fs.readLocalFile.mockResolvedValueOnce(null); + fs.readLocalFile.mockResolvedValueOnce('12.16.1\n'); + const res = await getNodeConstraint('package.json'); + expect(res).toEqual('12.16.1'); + }); + it('returns .nvmrc value', async () => { + fs.readLocalFile = jest.fn(); + fs.readLocalFile.mockResolvedValueOnce('12.16.2\n'); + const res = await getNodeConstraint('package.json'); + expect(res).toEqual('12.16.2'); + }); + it('ignores unusable ranges in dotfiles', async () => { + fs.readLocalFile = jest.fn(); + fs.readLocalFile.mockResolvedValueOnce('latest'); + fs.readLocalFile.mockResolvedValueOnce('lts'); + fs.readLocalFile.mockResolvedValueOnce('{"engines":{"node":"^12.16.0"}}'); + const res = await getNodeConstraint('package.json'); + expect(res).toEqual('^12.16.0'); + }); + it('returns no constraint', async () => { + fs.readLocalFile = jest.fn(); + fs.readLocalFile.mockResolvedValueOnce(null); + fs.readLocalFile.mockResolvedValueOnce(null); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const res = await getNodeConstraint('package.json'); + expect(res).toBeNull(); + }); +}); diff --git a/lib/manager/npm/post-update/node-version.ts b/lib/manager/npm/post-update/node-version.ts new file mode 100644 index 0000000000000000000000000000000000000000..28936276c935123be69c918da06a97a0f10b418f --- /dev/null +++ b/lib/manager/npm/post-update/node-version.ts @@ -0,0 +1,47 @@ +import { validRange } from 'semver'; +import { logger } from '../../../logger'; +import { getSiblingFileName, readLocalFile } from '../../../util/fs'; + +async function getNodeFile(filename: string): Promise<string> | null { + try { + const constraint = (await readLocalFile(filename, 'utf8')) + .split('\n')[0] + .replace(/^v/, ''); + if (validRange(constraint)) { + logger.debug(`Using node constraint "${constraint}" from ${filename}`); + return constraint; + } + } catch (err) { + // do nothing + } + return null; +} + +async function getPackageJsonConstraint( + filename: string +): Promise<string> | null { + try { + const pj = JSON.parse(await readLocalFile(filename, 'utf8')); + const constraint = pj?.engines?.node; + if (constraint && validRange(constraint)) { + logger.debug(`Using node constraint "${constraint}" from package.json`); + return constraint; + } + } catch (err) { + // do nothing + } + return null; +} + +export async function getNodeConstraint( + filename: string +): Promise<string> | null { + const constraint = + (await getNodeFile(getSiblingFileName(filename, '.nvmrc'))) || + (await getNodeFile(getSiblingFileName(filename, '.node-version'))) || + (await getPackageJsonConstraint(filename)); + if (!constraint) { + logger.debug('No node constraint found - using latest'); + } + return constraint; +} diff --git a/lib/manager/npm/post-update/npm.spec.ts b/lib/manager/npm/post-update/npm.spec.ts index f27e6db4e7f00a3d72afa8d9860d12b42f4ae9bc..83081050bd8c947333e10d0f6166cc4c27c2b572 100644 --- a/lib/manager/npm/post-update/npm.spec.ts +++ b/lib/manager/npm/post-update/npm.spec.ts @@ -10,6 +10,7 @@ import * as npmHelper from './npm'; jest.mock('fs-extra'); jest.mock('child_process'); jest.mock('../../../util/exec/env'); +jest.mock('./node-version'); const exec: jest.Mock<typeof _exec> = _exec as any; const env = mocked(_env); diff --git a/lib/manager/npm/post-update/npm.ts b/lib/manager/npm/post-update/npm.ts index abf0d19f4444ba3ca0d1f25a51c33ac8453294de..18511f8180fbd19e2cf2e9e65edf947859fb4914 100644 --- a/lib/manager/npm/post-update/npm.ts +++ b/lib/manager/npm/post-update/npm.ts @@ -4,6 +4,7 @@ import { SYSTEM_INSUFFICIENT_DISK_SPACE } from '../../../constants/error-message import { logger } from '../../../logger'; import { ExecOptions, exec } from '../../../util/exec'; import { PostUpdateConfig, Upgrade } from '../../common'; +import { getNodeConstraint } from './node-version'; export interface GenerateLockFileResult { error?: boolean; @@ -36,6 +37,7 @@ export async function generateLockFile( logger.debug('Updating lock file only'); cmdOptions += '--package-lock-only --no-audit'; } + const tagConstraint = await getNodeConstraint(config.packageFile); const execOptions: ExecOptions = { cwd, extraEnv: { @@ -44,6 +46,8 @@ export async function generateLockFile( }, docker: { image: 'renovate/node', + tagScheme: 'npm', + tagConstraint, preCommands, }, }; diff --git a/lib/manager/npm/post-update/pnpm.spec.ts b/lib/manager/npm/post-update/pnpm.spec.ts index 6eedd09a0b174063a677e1fe689934d18347e7fb..0c2991a4019d61c2d52611e6a975dd415687d43f 100644 --- a/lib/manager/npm/post-update/pnpm.spec.ts +++ b/lib/manager/npm/post-update/pnpm.spec.ts @@ -9,6 +9,7 @@ import * as _pnpmHelper from './pnpm'; jest.mock('fs-extra'); jest.mock('child_process'); jest.mock('../../../util/exec/env'); +jest.mock('./node-version'); const exec: jest.Mock<typeof _exec> = _exec as any; const env = mocked(_env); diff --git a/lib/manager/npm/post-update/pnpm.ts b/lib/manager/npm/post-update/pnpm.ts index 9b1d5383fd58e4d0921db50812326648fbc6f47d..aba81ac352996f15dc072f7276fd8a01b8d95ffd 100644 --- a/lib/manager/npm/post-update/pnpm.ts +++ b/lib/manager/npm/post-update/pnpm.ts @@ -3,6 +3,7 @@ import { join } from 'upath'; import { logger } from '../../../logger'; import { ExecOptions, exec } from '../../../util/exec'; import { PostUpdateConfig } from '../../common'; +import { getNodeConstraint } from './node-version'; export interface GenerateLockFileResult { error?: boolean; @@ -23,6 +24,7 @@ export async function generateLockFile( let cmd = 'pnpm'; try { const preCommands = ['npm i -g pnpm']; + const tagConstraint = await getNodeConstraint(config.packageFile); const execOptions: ExecOptions = { cwd, extraEnv: { @@ -31,6 +33,8 @@ export async function generateLockFile( }, docker: { image: 'renovate/node', + tagScheme: 'npm', + tagConstraint, preCommands, }, }; diff --git a/lib/manager/npm/post-update/yarn.spec.ts b/lib/manager/npm/post-update/yarn.spec.ts index 09f50f5eaa994c2c8fb9ce9d8b7346e5fd1466b3..2ad39862c96e0b5ee2813e6704c29b175a1475ed 100644 --- a/lib/manager/npm/post-update/yarn.spec.ts +++ b/lib/manager/npm/post-update/yarn.spec.ts @@ -8,6 +8,7 @@ import * as _yarnHelper from './yarn'; jest.mock('fs-extra'); jest.mock('child_process'); jest.mock('../../../util/exec/env'); +jest.mock('./node-version'); const exec: jest.Mock<typeof _exec> = _exec as any; const env = mocked(_env); diff --git a/lib/manager/npm/post-update/yarn.ts b/lib/manager/npm/post-update/yarn.ts index f2f70b482fdafce2027eadb1a4ea4b47d5001496..8a5c71a0ea9f7967e55a3982223e693d04b9f1da 100644 --- a/lib/manager/npm/post-update/yarn.ts +++ b/lib/manager/npm/post-update/yarn.ts @@ -6,6 +6,7 @@ import { DatasourceError } from '../../../datasource'; import { logger } from '../../../logger'; import { ExecOptions, exec } from '../../../util/exec'; import { PostUpdateConfig, Upgrade } from '../../common'; +import { getNodeConstraint } from './node-version'; export interface GenerateLockFileResult { error?: boolean; @@ -55,6 +56,7 @@ export async function generateLockFile( if (global.trustLevel !== 'high' || config.ignoreScripts) { cmdOptions += ' --ignore-scripts --ignore-engines --ignore-platform'; } + const tagConstraint = await getNodeConstraint(config.packageFile); const execOptions: ExecOptions = { cwd, extraEnv: { @@ -63,6 +65,8 @@ export async function generateLockFile( }, docker: { image: 'renovate/node', + tagScheme: 'npm', + tagConstraint, preCommands, }, };