From 62a540c272d4f0b2738781d0442d61fd946cb482 Mon Sep 17 00:00:00 2001 From: ylemkimon Date: Sun, 29 Aug 2021 21:59:58 +0900 Subject: [PATCH] feat(npm): use Yarn 3 mode to skip install or build (#11012) --- .../extract/__snapshots__/index.spec.ts.snap | 135 +++++++++++++++++ .../locked-versions.spec.ts.snap | 34 +++++ lib/manager/npm/extract/index.spec.ts | 20 +++ lib/manager/npm/extract/index.ts | 8 +- .../npm/extract/locked-versions.spec.ts | 80 +++++----- lib/manager/npm/extract/locked-versions.ts | 5 +- lib/manager/npm/extract/yarn.ts | 32 +++- .../__snapshots__/yarn.spec.ts.snap | 143 ++++++++++++++++++ lib/manager/npm/post-update/index.ts | 13 +- lib/manager/npm/post-update/yarn.spec.ts | 39 ++++- lib/manager/npm/post-update/yarn.ts | 44 ++++-- 11 files changed, 481 insertions(+), 72 deletions(-) diff --git a/lib/manager/npm/extract/__snapshots__/index.spec.ts.snap b/lib/manager/npm/extract/__snapshots__/index.spec.ts.snap index eab021af9396fd..30f6f6e5bac7e4 100644 --- a/lib/manager/npm/extract/__snapshots__/index.spec.ts.snap +++ b/lib/manager/npm/extract/__snapshots__/index.spec.ts.snap @@ -1339,3 +1339,138 @@ Object { "yarnWorkspacesPackages": Array [], } `; + +exports[`manager/npm/extract/index .extractPackageFile() sets skipInstalls false if Yarn zero-install is used 1`] = ` +Object { + "constraints": Object {}, + "deps": Array [ + Object { + "currentValue": "6.5.0", + "datasource": "npm", + "depName": "autoprefixer", + "depType": "dependencies", + "prettyDepType": "dependency", + }, + Object { + "currentValue": "~1.6.0", + "datasource": "npm", + "depName": "bower", + "depType": "dependencies", + "prettyDepType": "dependency", + }, + Object { + "currentValue": "13.1.0", + "datasource": "npm", + "depName": "browserify", + "depType": "dependencies", + "prettyDepType": "dependency", + }, + Object { + "currentValue": "0.9.2", + "datasource": "npm", + "depName": "browserify-css", + "depType": "dependencies", + "prettyDepType": "dependency", + }, + Object { + "currentValue": "=0.22.0", + "datasource": "npm", + "depName": "cheerio", + "depType": "dependencies", + "prettyDepType": "dependency", + }, + Object { + "currentValue": "1.21.0", + "datasource": "npm", + "depName": "config", + "depType": "dependencies", + "prettyDepType": "dependency", + }, + Object { + "depName": "enabled", + "depType": "devDependencies", + "prettyDepType": "devDependency", + "skipReason": "invalid-value", + }, + Object { + "currentValue": "^1.5.8", + "datasource": "npm", + "depName": "angular", + "depType": "devDependencies", + "prettyDepType": "devDependency", + }, + Object { + "currentValue": "1.5.8", + "datasource": "npm", + "depName": "angular-touch", + "depType": "devDependencies", + "prettyDepType": "devDependency", + }, + Object { + "currentValue": "1.5.8", + "datasource": "npm", + "depName": "angular-sanitize", + "depType": "devDependencies", + "prettyDepType": "devDependency", + }, + Object { + "currentValue": "4.0.0-beta.1", + "datasource": "npm", + "depName": "@angular/core", + "depType": "devDependencies", + "prettyDepType": "devDependency", + }, + Object { + "currentValue": "1.21.0", + "datasource": "npm", + "depName": "config", + "depType": "resolutions", + "prettyDepType": "resolutions", + }, + Object { + "currentValue": "8.0.0", + "datasource": "npm", + "depName": "@angular/cli", + "depType": "resolutions", + "managerData": Object { + "key": "**/@angular/cli", + }, + "prettyDepType": "resolutions", + }, + Object { + "currentValue": "1.33.0", + "datasource": "npm", + "depName": "angular", + "depType": "resolutions", + "managerData": Object { + "key": "**/angular", + }, + "prettyDepType": "resolutions", + }, + Object { + "currentValue": "1.0.0", + "datasource": "npm", + "depName": "glob", + "depType": "resolutions", + "managerData": Object { + "key": "config/glob", + }, + "prettyDepType": "resolutions", + }, + ], + "lernaClient": undefined, + "lernaPackages": undefined, + "managerData": Object { + "lernaJsonFile": undefined, + }, + "npmLock": undefined, + "npmrc": undefined, + "packageFileVersion": "1.0.0", + "packageJsonName": "renovate", + "packageJsonType": "app", + "pnpmShrinkwrap": undefined, + "skipInstalls": false, + "yarnLock": "yarn.lock", + "yarnWorkspacesPackages": undefined, +} +`; diff --git a/lib/manager/npm/extract/__snapshots__/locked-versions.spec.ts.snap b/lib/manager/npm/extract/__snapshots__/locked-versions.spec.ts.snap index 09abf98ca4105e..89d0ea80b82c66 100644 --- a/lib/manager/npm/extract/__snapshots__/locked-versions.spec.ts.snap +++ b/lib/manager/npm/extract/__snapshots__/locked-versions.spec.ts.snap @@ -195,3 +195,37 @@ Array [ }, ] `; + +exports[`manager/npm/extract/locked-versions .getLockedVersions() uses yarn.lock with yarn v3.0.0 1`] = ` +Array [ + Object { + "constraints": Object { + "yarn": "^3.0.0", + }, + "deps": Array [ + Object { + "currentValue": "1.0.0", + "depName": "a", + "lockedVersion": "1.0.0", + }, + Object { + "currentValue": "2.0.0", + "depName": "b", + "lockedVersion": "2.0.0", + }, + Object { + "currentValue": "^3.0.0", + "depName": "yarn", + "depType": "engines", + "lockedVersion": undefined, + "lookupName": "@yarnpkg/cli", + }, + ], + "lockFiles": Array [ + "yarn.lock", + ], + "npmLock": "package-lock.json", + "yarnLock": "yarn.lock", + }, +] +`; diff --git a/lib/manager/npm/extract/index.spec.ts b/lib/manager/npm/extract/index.spec.ts index 9b77cf11a5bd95..f67f4420b6d272 100644 --- a/lib/manager/npm/extract/index.spec.ts +++ b/lib/manager/npm/extract/index.spec.ts @@ -22,6 +22,7 @@ describe('manager/npm/extract/index', () => { describe('.extractPackageFile()', () => { beforeEach(() => { fs.readLocalFile = jest.fn(() => null); + fs.localPathExists = jest.fn(() => false); }); it('returns null if cannot parse', async () => { const res = await npmExtract.extractPackageFile( @@ -365,6 +366,25 @@ describe('manager/npm/extract/index', () => { // FIXME: explicit assert condition expect(res).toMatchSnapshot(); }); + + it('sets skipInstalls false if Yarn zero-install is used', async () => { + fs.readLocalFile = jest.fn((fileName) => { + if (fileName === 'yarn.lock') { + return '# yarn.lock'; + } + if (fileName === '.yarnrc.yml') { + return 'pnpEnableInlining: false'; + } + return null; + }); + fs.localPathExists = jest.fn(() => true); + const res = await npmExtract.extractPackageFile( + input01Content, + 'package.json', + defaultConfig + ); + expect(res).toMatchSnapshot(); + }); }); describe('.postExtract()', () => { it('runs', async () => { diff --git a/lib/manager/npm/extract/index.ts b/lib/manager/npm/extract/index.ts index 861af94a09ff9d..ebeb46e3c0e091 100644 --- a/lib/manager/npm/extract/index.ts +++ b/lib/manager/npm/extract/index.ts @@ -19,6 +19,7 @@ import { getLockedVersions } from './locked-versions'; import { detectMonorepos } from './monorepo'; import { mightBeABrowserLibrary } from './type'; import type { NpmPackage, NpmPackageDependency } from './types'; +import { isZeroInstall } from './yarn'; function parseDepName(depType: string, key: string): string { if (depType !== 'resolutions') { @@ -117,6 +118,10 @@ export async function extractPackageFile( } } } + + const yarnrcYmlFileName = getSiblingFileName(fileName, '.yarnrc.yml'); + const yarnZeroInstall = await isZeroInstall(yarnrcYmlFileName); + let lernaJsonFile: string; let lernaPackages: string[]; let lernaClient: 'yarn' | 'npm'; @@ -342,11 +347,12 @@ export async function extractPackageFile( } let skipInstalls = config.skipInstalls; if (skipInstalls === null) { - if (hasFancyRefs && lockFiles.npmLock) { + if ((hasFancyRefs && lockFiles.npmLock) || yarnZeroInstall) { // https://github.com/npm/cli/issues/1432 // Explanation: // - npm install --package-lock-only is buggy for transitive deps in file: and npm: references // - So we set skipInstalls to false if file: or npm: refs are found *and* the user hasn't explicitly set the value already + // - Also, do not skip install if Yarn zero-install is used logger.debug('Automatically setting skipInstalls to false'); skipInstalls = false; } else { diff --git a/lib/manager/npm/extract/locked-versions.spec.ts b/lib/manager/npm/extract/locked-versions.spec.ts index 2a1d5f5eb0f2dc..3fff64362a4e0d 100644 --- a/lib/manager/npm/extract/locked-versions.spec.ts +++ b/lib/manager/npm/extract/locked-versions.spec.ts @@ -10,45 +10,47 @@ jest.mock('./yarn'); describe('manager/npm/extract/locked-versions', () => { describe('.getLockedVersions()', () => { - it.each([['1.22.0'], ['2.1.0'], ['2.2.0']])( - 'uses yarn.lock with yarn v%s', - async (yarnVersion) => { - yarn.getYarnLock.mockReturnValue({ - isYarn1: yarnVersion === '1.22.0', - lockfileVersion: yarnVersion === '2.2.0' ? 6 : undefined, - lockedVersions: { - 'a@1.0.0': '1.0.0', - 'b@2.0.0': '2.0.0', - 'c@2.0.0': '3.0.0', - }, - }); - const packageFiles = [ - { - npmLock: 'package-lock.json', - yarnLock: 'yarn.lock', - constraints: {}, - deps: [ - { - depName: 'a', - currentValue: '1.0.0', - }, - { - depName: 'b', - currentValue: '2.0.0', - }, - { - depType: 'engines', - depName: 'yarn', - currentValue: `^${yarnVersion}`, - }, - ], - }, - ]; - await getLockedVersions(packageFiles); - // FIXME: explicit assert condition - expect(packageFiles).toMatchSnapshot(); - } - ); + it.each([ + ['1.22.0', undefined], + ['2.1.0', undefined], + ['2.2.0', 6], + ['3.0.0', 8], + ])('uses yarn.lock with yarn v%s', async (yarnVersion, lockfileVersion) => { + yarn.getYarnLock.mockReturnValue({ + isYarn1: yarnVersion === '1.22.0', + lockfileVersion, + lockedVersions: { + 'a@1.0.0': '1.0.0', + 'b@2.0.0': '2.0.0', + 'c@2.0.0': '3.0.0', + }, + }); + const packageFiles = [ + { + npmLock: 'package-lock.json', + yarnLock: 'yarn.lock', + constraints: {}, + deps: [ + { + depName: 'a', + currentValue: '1.0.0', + }, + { + depName: 'b', + currentValue: '2.0.0', + }, + { + depType: 'engines', + depName: 'yarn', + currentValue: `^${yarnVersion}`, + }, + ], + }, + ]; + await getLockedVersions(packageFiles); + // FIXME: explicit assert condition + expect(packageFiles).toMatchSnapshot(); + }); it.each([['6.0.0'], ['7.0.0']])( 'uses package-lock.json with npm v%s', diff --git a/lib/manager/npm/extract/locked-versions.ts b/lib/manager/npm/extract/locked-versions.ts index 69e98c4ca3065f..2b95bc450ee0d1 100644 --- a/lib/manager/npm/extract/locked-versions.ts +++ b/lib/manager/npm/extract/locked-versions.ts @@ -22,7 +22,10 @@ export async function getLockedVersions( } const { lockfileVersion, isYarn1 } = lockFileCache[yarnLock]; if (!isYarn1) { - if (lockfileVersion >= 6) { + if (lockfileVersion >= 8) { + // https://github.com/yarnpkg/berry/commit/9bcd27ae34aee77a567dd104947407532fa179b3 + packageFile.constraints.yarn = '^3.0.0'; + } else if (lockfileVersion >= 6) { // https://github.com/yarnpkg/berry/commit/f753790380cbda5b55d028ea84b199445129f9ba packageFile.constraints.yarn = '^2.2.0'; } else { diff --git a/lib/manager/npm/extract/yarn.ts b/lib/manager/npm/extract/yarn.ts index c685166010ee5e..56d9f64b9432b4 100644 --- a/lib/manager/npm/extract/yarn.ts +++ b/lib/manager/npm/extract/yarn.ts @@ -1,7 +1,12 @@ -import { structUtils } from '@yarnpkg/core'; +import is from '@sindresorhus/is'; +import { miscUtils, structUtils } from '@yarnpkg/core'; import { parseSyml } from '@yarnpkg/parsers'; import { logger } from '../../../logger'; -import { readLocalFile } from '../../../util/fs'; +import { + getSiblingFileName, + localPathExists, + readLocalFile, +} from '../../../util/fs'; import type { LockFile } from './types'; export async function getYarnLock(filePath: string): Promise { @@ -36,3 +41,26 @@ export async function getYarnLock(filePath: string): Promise { return { isYarn1: true, lockedVersions: {} }; } } + +export function getZeroInstallPaths(yarnrcYml: string): string[] { + const conf = parseSyml(yarnrcYml); + const paths = [conf.cacheFolder || './.yarn/cache', '.pnp.cjs', '.pnp.js']; + if (miscUtils.tryParseOptionalBoolean(conf.pnpEnableInlining) === false) { + paths.push(conf.pnpDataPath || './.pnp.data.json'); + } + return paths; +} + +export async function isZeroInstall(yarnrcYmlPath: string): Promise { + const yarnrcYml = await readLocalFile(yarnrcYmlPath, 'utf8'); + if (is.string(yarnrcYml)) { + const paths = getZeroInstallPaths(yarnrcYml); + for (const p of paths) { + if (await localPathExists(getSiblingFileName(yarnrcYmlPath, p))) { + logger.debug(`Detected Yarn zero-install in ${p}`); + return true; + } + } + } + return false; +} diff --git a/lib/manager/npm/post-update/__snapshots__/yarn.spec.ts.snap b/lib/manager/npm/post-update/__snapshots__/yarn.spec.ts.snap index c7397cf62a5432..2ddd158992983b 100644 --- a/lib/manager/npm/post-update/__snapshots__/yarn.spec.ts.snap +++ b/lib/manager/npm/post-update/__snapshots__/yarn.spec.ts.snap @@ -222,6 +222,102 @@ Array [ ] `; +exports[`manager/npm/post-update/yarn generates lock files using yarn v3.0.0 1`] = ` +Array [ + Object { + "cmd": "yarn install --mode=update-lockfile", + "options": Object { + "cwd": "some-dir", + "encoding": "utf-8", + "env": Object { + "CI": "true", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + "YARN_ENABLE_IMMUTABLE_INSTALLS": "false", + "YARN_GLOBAL_FOLDER": "/tmp/renovate/cache/berry", + "YARN_HTTP_TIMEOUT": "100000", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, + Object { + "cmd": "yarn dedupe --strategy highest --mode=update-lockfile", + "options": Object { + "cwd": "some-dir", + "encoding": "utf-8", + "env": Object { + "CI": "true", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + "YARN_ENABLE_IMMUTABLE_INSTALLS": "false", + "YARN_GLOBAL_FOLDER": "/tmp/renovate/cache/berry", + "YARN_HTTP_TIMEOUT": "100000", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`manager/npm/post-update/yarn only skips build if skipInstalls is false 1`] = ` +Array [ + Object { + "cmd": "yarn install --mode=skip-build", + "options": Object { + "cwd": "some-dir", + "encoding": "utf-8", + "env": Object { + "CI": "true", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + "YARN_ENABLE_IMMUTABLE_INSTALLS": "false", + "YARN_HTTP_TIMEOUT": "100000", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, + Object { + "cmd": "yarn dedupe --strategy highest --mode=skip-build", + "options": Object { + "cwd": "some-dir", + "encoding": "utf-8", + "env": Object { + "CI": "true", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + "YARN_ENABLE_IMMUTABLE_INSTALLS": "false", + "YARN_HTTP_TIMEOUT": "100000", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + exports[`manager/npm/post-update/yarn performs lock file maintenance using yarn v1.22.0 1`] = ` Array [ Object { @@ -532,3 +628,50 @@ Array [ }, ] `; + +exports[`manager/npm/post-update/yarn performs lock file updates using yarn v3.0.0 1`] = ` +Array [ + Object { + "cmd": "yarn install --mode=update-lockfile", + "options": Object { + "cwd": "some-dir", + "encoding": "utf-8", + "env": Object { + "CI": "true", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + "YARN_ENABLE_IMMUTABLE_INSTALLS": "false", + "YARN_HTTP_TIMEOUT": "100000", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, + Object { + "cmd": "yarn up some-dep@^1.0.0 --mode=update-lockfile", + "options": Object { + "cwd": "some-dir", + "encoding": "utf-8", + "env": Object { + "CI": "true", + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + "YARN_ENABLE_IMMUTABLE_INSTALLS": "false", + "YARN_HTTP_TIMEOUT": "100000", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; diff --git a/lib/manager/npm/post-update/index.ts b/lib/manager/npm/post-update/index.ts index 63e95231f534f5..0742fbafbc398d 100644 --- a/lib/manager/npm/post-update/index.ts +++ b/lib/manager/npm/post-update/index.ts @@ -1,5 +1,4 @@ import is from '@sindresorhus/is'; -import { parseSyml } from '@yarnpkg/parsers'; import deepmerge from 'deepmerge'; import { dump, load } from 'js-yaml'; import upath from 'upath'; @@ -25,6 +24,7 @@ import { import { branchExists, getFile, getRepoStatus } from '../../../util/git'; import * as hostRules from '../../../util/host-rules'; import type { PackageFile, PostUpdateConfig, Upgrade } from '../../types'; +import { getZeroInstallPaths } from '../extract/yarn'; import * as lerna from './lerna'; import * as npm from './npm'; import * as pnpm from './pnpm'; @@ -330,15 +330,8 @@ async function updateYarnOffline( // both files may exist, so check for .yarnrc.yml first if (yarnrcYml) { // Yarn 2 (offline cache and zero-installs) - const config = parseSyml(yarnrcYml); - resolvedPaths.push( - upath.join(lockFileDir, config.cacheFolder || './.yarn/cache') - ); - - resolvedPaths.push(upath.join(lockFileDir, '.pnp')); - if (config.pnpDataPath) { - resolvedPaths.push(upath.join(lockFileDir, config.pnpDataPath)); - } + const paths = getZeroInstallPaths(yarnrcYml); + resolvedPaths.push(...paths.map((p) => upath.join(lockFileDir, p))); } else if (yarnrc) { // Yarn 1 (offline mirror) const mirrorLine = yarnrc diff --git a/lib/manager/npm/post-update/yarn.spec.ts b/lib/manager/npm/post-update/yarn.spec.ts index bf3beecd67ba51..9b950ada230be6 100644 --- a/lib/manager/npm/post-update/yarn.spec.ts +++ b/lib/manager/npm/post-update/yarn.spec.ts @@ -32,10 +32,12 @@ describe('manager/npm/post-update/yarn', () => { jest.resetModules(); env.getChildProcessEnv.mockReturnValue(envMock.basic); }); + it.each([ ['1.22.0', '^1.10.0', 2], ['2.1.0', '>= 2.0.0', 1], ['2.2.0', '2.2.0', 1], + ['3.0.0', '3.0.0', 1], ])( 'generates lock files using yarn v%s', async (yarnVersion, yarnCompatibility, expectedFsCalls) => { @@ -73,9 +75,35 @@ describe('manager/npm/post-update/yarn', () => { expect(fixSnapshots(execSnapshots)).toMatchSnapshot(); } ); - it.each([['1.22.0'], ['2.1.0']])( + + it('only skips build if skipInstalls is false', async () => { + const execSnapshots = mockExecAll(exec, { + stdout: '3.0.0', + stderr: '', + }); + fs.readFile.mockImplementation( + (filename, encoding) => + new Promise((resolve) => resolve('package-lock-contents')) + ); + const config = { + constraints: { + yarn: '3.0.0', + }, + postUpdateOptions: ['yarnDedupeFewer', 'yarnDedupeHighest'], + skipInstalls: false, + }; + const res = await yarnHelper.generateLockFile('some-dir', {}, config); + expect(res.lockFile).toEqual('package-lock-contents'); + expect(fixSnapshots(execSnapshots)).toMatchSnapshot(); + }); + + it.each([ + ['1.22.0', '^1.10.0'], + ['2.1.0', '>= 2.0.0'], + ['3.0.0', '3.0.0'], + ])( 'performs lock file updates using yarn v%s', - async (yarnVersion) => { + async (yarnVersion, yarnCompatibility) => { const execSnapshots = mockExecAll(exec, { stdout: yarnVersion, stderr: '', @@ -90,7 +118,7 @@ describe('manager/npm/post-update/yarn', () => { }); const config = { constraints: { - yarn: yarnVersion === '1.22.0' ? '^1.10.0' : '>= 2.0.0', + yarn: yarnCompatibility, }, }; const res = await yarnHelper.generateLockFile('some-dir', {}, config, [ @@ -104,6 +132,7 @@ describe('manager/npm/post-update/yarn', () => { expect(fixSnapshots(execSnapshots)).toMatchSnapshot(); } ); + it.each([['1.22.0']])( 'performs lock file updates and full install using yarn v%s', async (yarnVersion) => { @@ -126,6 +155,7 @@ describe('manager/npm/post-update/yarn', () => { expect(fixSnapshots(execSnapshots)).toMatchSnapshot(); } ); + it.each([ ['1.22.0', '^1.10.0', 2], ['2.1.0', '>= 2.0.0', 1], @@ -160,6 +190,7 @@ describe('manager/npm/post-update/yarn', () => { expect(fixSnapshots(execSnapshots)).toMatchSnapshot(); } ); + it('catches errors', async () => { const execSnapshots = mockExecAll(exec, { stdout: '1.9.4', @@ -174,6 +205,7 @@ describe('manager/npm/post-update/yarn', () => { expect(res.lockFile).not.toBeDefined(); expect(fixSnapshots(execSnapshots)).toMatchSnapshot(); }); + describe('checkYarnrc()', () => { it('returns offline mirror and yarn path', async () => { fs.readFile.mockImplementation((filename, encoding) => { @@ -189,6 +221,7 @@ describe('manager/npm/post-update/yarn', () => { // FIXME: explicit assert condition expect(await _yarnHelper.checkYarnrc('/tmp/renovate')).toMatchSnapshot(); }); + it('returns no offline mirror and unquoted yarn path', async () => { fs.readFile.mockImplementation((filename, encoding) => { if (filename.endsWith('.yarnrc')) { diff --git a/lib/manager/npm/post-update/yarn.ts b/lib/manager/npm/post-update/yarn.ts index f603151fc04256..5166f8ce47ffcc 100644 --- a/lib/manager/npm/post-update/yarn.ts +++ b/lib/manager/npm/post-update/yarn.ts @@ -63,6 +63,7 @@ export async function generateLockFile( const isYarn1 = !minYarnVersion || minYarnVersion.major === 1; const isYarnDedupeAvailable = minYarnVersion && gte(minYarnVersion, '2.2.0'); + const isYarnModeAvailable = minYarnVersion && gte(minYarnVersion, '3.0.0'); let installYarn = 'npm i -g yarn'; if (isYarn1 && minYarnVersion) { @@ -77,22 +78,28 @@ export async function generateLockFile( CI: 'true', }; - if (isYarn1 && config.skipInstalls !== false) { - const { offlineMirror, yarnPath } = await checkYarnrc(cwd); - if (!offlineMirror) { - logger.debug('Updating yarn.lock only - skipping node_modules'); - // The following change causes Yarn 1.x to exit gracefully after updating the lock file but without installing node_modules - preCommands.push(getOptimizeCommand()); - if (yarnPath) { - preCommands.push(getOptimizeCommand(yarnPath) + ' || true'); + const commands = []; + let cmdOptions = ''; // should have a leading space + if (config.skipInstalls !== false) { + if (isYarn1) { + const { offlineMirror, yarnPath } = await checkYarnrc(cwd); + if (!offlineMirror) { + logger.debug('Updating yarn.lock only - skipping node_modules'); + // The following change causes Yarn 1.x to exit gracefully after updating the lock file but without installing node_modules + preCommands.push(getOptimizeCommand()); + if (yarnPath) { + preCommands.push(getOptimizeCommand(yarnPath) + ' || true'); + } } + } else if (isYarnModeAvailable) { + // Don't run the link step and only fetch what's necessary to compute an updated lockfile + cmdOptions += ' --mode=update-lockfile'; } } - const commands = []; - let cmdOptions = ''; + if (isYarn1) { cmdOptions += - '--ignore-engines --ignore-platform --network-timeout 100000'; + ' --ignore-engines --ignore-platform --network-timeout 100000'; extraEnv.YARN_CACHE_FOLDER = env.YARN_CACHE_FOLDER; } else { extraEnv.YARN_ENABLE_IMMUTABLE_INSTALLS = 'false'; @@ -102,6 +109,11 @@ export async function generateLockFile( if (!getGlobalConfig().allowScripts || config.ignoreScripts) { if (isYarn1) { cmdOptions += ' --ignore-scripts'; + } else if (isYarnModeAvailable) { + if (config.skipInstalls === false) { + // Don't run the build scripts + cmdOptions += ' --mode=skip-build'; + } } else { extraEnv.YARN_ENABLE_SCRIPTS = '0'; } @@ -124,7 +136,7 @@ export async function generateLockFile( } // This command updates the lock file based on package.json - commands.push(`yarn install ${cmdOptions}`.trim()); + commands.push(`yarn install${cmdOptions}`); // rangeStrategy = update-lockfile const lockUpdates = upgrades.filter((upgrade) => upgrade.isLockfileUpdate); @@ -136,14 +148,14 @@ export async function generateLockFile( commands.push( `yarn upgrade ${lockUpdates .map((update) => update.depName) - .join(' ')} ${cmdOptions}`.trim() + .join(' ')}${cmdOptions}` ); } else { // `yarn up` updates to the latest release, so the range should be specified commands.push( `yarn up ${lockUpdates .map((update) => `${update.depName}@${update.newValue}`) - .join(' ')}` + .join(' ')}${cmdOptions}` ); } } @@ -159,9 +171,9 @@ export async function generateLockFile( if (isYarn1) { commands.push(`npx yarn-deduplicate --strategy ${s}`); // Run yarn again in case any changes are necessary - commands.push(`yarn install ${cmdOptions}`.trim()); + commands.push(`yarn install${cmdOptions}`); } else if (isYarnDedupeAvailable && s === 'highest') { - commands.push(`yarn dedupe --strategy ${s}`); + commands.push(`yarn dedupe --strategy ${s}${cmdOptions}`); } else { logger.debug(`yarn dedupe ${s} not available`); }