diff --git a/lib/workers/dep-type/index.js b/lib/workers/dep-type/index.js index a442905043eeead1f2b091f3078f0b25ee255e82..d4c1e139c2a825a0253fcc1de5437bb0cf751c07 100644 --- a/lib/workers/dep-type/index.js +++ b/lib/workers/dep-type/index.js @@ -11,7 +11,12 @@ module.exports = { getDepConfig, }; -async function renovateDepType(packageContent, config) { +async function renovateDepType( + packageContent, + config, + packageLockParsed, + yarnLockParsed +) { logger.setMeta({ repository: config.repository, packageFile: config.packageFile, @@ -27,7 +32,9 @@ async function renovateDepType(packageContent, config) { // Extract all dependencies from the package.json deps = await packageJson.extractDependencies( packageContent, - config.depType + config.depType, + packageLockParsed, + yarnLockParsed ); if (config.lerna || config.workspaces || config.workspaceDir) { deps = deps.filter( diff --git a/lib/workers/dep-type/package-json.js b/lib/workers/dep-type/package-json.js index 9f76f0e5dd8c03580ef7ee8a7c841b728faafb2a..a5c74cfdbd514b3fa18fa7fbf29961ab9e76f2b7 100644 --- a/lib/workers/dep-type/package-json.js +++ b/lib/workers/dep-type/package-json.js @@ -2,13 +2,51 @@ module.exports = { extractDependencies, }; -function extractDependencies(packageJson, depType) { +function extractDependencies( + packageJson, + depType, + packageLockParsed, + yarnLockParsed +) { const depNames = packageJson[depType] ? Object.keys(packageJson[depType]) : []; - return depNames.map(depName => ({ - depType, - depName, - currentVersion: packageJson[depType][depName].trim().replace(/^=/, ''), - })); + return depNames.map(depName => { + const currentVersion = packageJson[depType][depName] + .trim() + .replace(/^=/, ''); + let lockedVersion; + try { + logger.debug('Looking for locked version'); + if (packageLockParsed) { + logger.debug({ currentVersion }, 'Found parsed package-lock.json'); + if (packageLockParsed.dependencies[depName]) { + logger.debug('Found match'); + lockedVersion = packageLockParsed.dependencies[depName].version; + } else { + logger.debug('No match'); + } + } else if (yarnLockParsed && yarnLockParsed.object) { + logger.debug({ currentVersion }, 'Found parsed yarn.lock'); + const key = `${depName}@${currentVersion}`; + const lockEntry = yarnLockParsed.object[key]; + if (lockEntry) { + logger.debug('Found match'); + lockedVersion = lockEntry.version; + } else { + logger.debug('No match'); + } + } else { + logger.warn('No lock file found'); + } + } catch (err) { + logger.debug({ currentVersion }, 'Could not find locked version'); + } + return { + depType, + depName, + currentVersion, + lockedVersion, + }; + }); } diff --git a/lib/workers/package-file/index.js b/lib/workers/package-file/index.js index 88423b02c9d5542d7d10c1bf267ac82c39701254..3ae98c5fa3f508d40852735cd911db4da6556e3a 100644 --- a/lib/workers/package-file/index.js +++ b/lib/workers/package-file/index.js @@ -1,6 +1,8 @@ +const yarnLockParser = require('@yarnpkg/lockfile'); const configParser = require('../../config'); const depTypeWorker = require('../dep-type'); const npmApi = require('../../manager/npm/registry'); +const upath = require('upath'); module.exports = { mightBeABrowserLibrary, @@ -49,6 +51,41 @@ async function renovatePackageFile(packageFileConfig) { return upgrades; } + let yarnLockParsed; + let packageLockParsed; + let { yarnLock } = config; + if (!yarnLock && config.workspaceDir) { + yarnLock = upath.join(config.workspaceDir, 'yarn.lock'); + logger.debug({ yarnLock }, 'Using workspaces yarn.lock'); + } + if (yarnLock) { + try { + yarnLockParsed = yarnLockParser.parse(await platform.getFile(yarnLock)); + if (yarnLockParsed.type !== 'success') { + logger.warn( + { type: yarnLockParsed.type }, + 'Error parsing yarn.lock - not success' + ); + yarnLockParsed = undefined; + } + logger.info({ yarnLockParsed }); + } catch (err) { + logger.warn({ yarnLock }, 'Could not parse yarn.lock'); + } + } else if (config.packageLock) { + try { + packageLockParsed = JSON.parse( + await platform.getFile(config.packageLock) + ); + logger.info({ packageLockParsed }); + } catch (err) { + logger.warn( + { packageLock: config.packageLock }, + 'Could not parse package-lock.json' + ); + } + } + const depTypes = [ 'dependencies', 'devDependencies', @@ -76,7 +113,12 @@ async function renovatePackageFile(packageFileConfig) { logger.trace({ config: depTypeConfigs }, `depTypeConfigs`); for (const depTypeConfig of depTypeConfigs) { upgrades = upgrades.concat( - await depTypeWorker.renovateDepType(config.content, depTypeConfig) + await depTypeWorker.renovateDepType( + config.content, + depTypeConfig, + packageLockParsed, + yarnLockParsed + ) ); } // Reset logger again diff --git a/lib/workers/package/versions.js b/lib/workers/package/versions.js index 04f77db3817a48280f4756053211cf89c79e5a82..5aa0b343342f1d8b9733a9c3b1c5f0d35e402541 100644 --- a/lib/workers/package/versions.js +++ b/lib/workers/package/versions.js @@ -17,7 +17,7 @@ function determineUpgrades(npmDep, config) { const result = { type: 'warning', }; - const { currentVersion } = config; + const { currentVersion, lockedVersion } = config; const { versions } = npmDep; if (!versions || Object.keys(versions).length === 0) { result.message = `No versions returned from registry for this package`; @@ -29,25 +29,35 @@ function determineUpgrades(npmDep, config) { let changeLogFromVersion = currentVersion; // Check for a current range and pin it if (isRange(currentVersion)) { - // Pin ranges to their maximum satisfying version - logger.debug({ dependency: npmDep.name }, 'currentVersion is range'); - const maxSatisfying = semver.maxSatisfying(versionList, currentVersion); - if (!maxSatisfying) { - result.message = `No satisfying version found for existing dependency range "${currentVersion}"`; - logger.info( - { dependency: npmDep.name, currentVersion }, - `Warning: ${result.message}` + let newVersion; + if (lockedVersion) { + newVersion = lockedVersion; + } else { + // Pin ranges to their maximum satisfying version + logger.debug( + { dependency: npmDep.name }, + 'currentVersion is range, not locked' ); - return [result]; + const maxSatisfying = semver.maxSatisfying(versionList, currentVersion); + if (!maxSatisfying) { + result.message = `No satisfying version found for existing dependency range "${currentVersion}"`; + logger.info( + { dependency: npmDep.name, currentVersion }, + `Warning: ${result.message}` + ); + return [result]; + } + logger.debug({ maxSatisfying }); + newVersion = maxSatisfying; } - logger.debug({ maxSatisfying }); + allUpgrades.pin = { type: 'pin', isPin: true, - newVersion: maxSatisfying, - newVersionMajor: semver.major(maxSatisfying), + newVersion, + newVersionMajor: semver.major(newVersion), }; - changeLogFromVersion = maxSatisfying; + changeLogFromVersion = newVersion; } else if (versionList.indexOf(currentVersion) === -1) { logger.debug({ dependency: npmDep.name }, 'Cannot find currentVersion'); try { diff --git a/package.json b/package.json index d3e05d9dac8d5c2244905bb67c46c6fd48962148..76a4214afdb53d5a0c229d28eb7e543c2da8a8a0 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "npm": "5" }, "dependencies": { + "@yarnpkg/lockfile": "1.0.0", "bunyan": "1.8.12", "cacache": "10.0.2", "chalk": "2.3.0", diff --git a/test/workers/dep-type/__snapshots__/package-json.spec.js.snap b/test/workers/dep-type/__snapshots__/package-json.spec.js.snap index c436f35edd414da2fcf9d00bc4eda65b1a669941..a5113479c14d7ab72952e7a50fd9688cf48f51fe 100644 --- a/test/workers/dep-type/__snapshots__/package-json.spec.js.snap +++ b/test/workers/dep-type/__snapshots__/package-json.spec.js.snap @@ -6,31 +6,37 @@ Array [ "currentVersion": "6.5.0", "depName": "autoprefixer", "depType": "dependencies", + "lockedVersion": undefined, }, Object { "currentVersion": "~1.6.0", "depName": "bower", "depType": "dependencies", + "lockedVersion": undefined, }, Object { "currentVersion": "13.1.0", "depName": "browserify", "depType": "dependencies", + "lockedVersion": undefined, }, Object { "currentVersion": "0.9.2", "depName": "browserify-css", "depType": "dependencies", + "lockedVersion": undefined, }, Object { "currentVersion": "0.22.0", "depName": "cheerio", "depType": "dependencies", + "lockedVersion": undefined, }, Object { "currentVersion": "1.21.0", "depName": "config", "depType": "dependencies", + "lockedVersion": undefined, }, ] `; diff --git a/test/workers/dep-type/package-json.spec.js b/test/workers/dep-type/package-json.spec.js index 1538cbb0db168c3be321289748e16c2c2416d173..235a386c02a8cf7d1e924901c8dbeda205c0ee2b 100644 --- a/test/workers/dep-type/package-json.spec.js +++ b/test/workers/dep-type/package-json.spec.js @@ -56,5 +56,45 @@ describe('workers/dep-type/package-json', () => { extractedDependencies.should.be.instanceof(Array); extractedDependencies.should.have.length(0); }); + it('finds a locked version in package-lock.json', () => { + const packageLockParsed = { + dependencies: { chalk: { version: '2.0.1' } }, + }; + const extractedDependencies = packageJson.extractDependencies( + { dependencies: { chalk: '^2.0.0', foo: '^1.0.0' } }, + 'dependencies', + packageLockParsed + ); + extractedDependencies.should.be.instanceof(Array); + extractedDependencies.should.have.length(2); + expect(extractedDependencies[0].lockedVersion).toBeDefined(); + expect(extractedDependencies[1].lockedVersion).toBeUndefined(); + }); + it('finds a locked version in yarn.lock', () => { + const yarnLockParsed = { + object: { 'chalk@^2.0.0': { version: '2.0.1' } }, + }; + const extractedDependencies = packageJson.extractDependencies( + { dependencies: { chalk: '^2.0.0', foo: '^1.0.0' } }, + 'dependencies', + undefined, + yarnLockParsed + ); + extractedDependencies.should.be.instanceof(Array); + extractedDependencies.should.have.length(2); + expect(extractedDependencies[0].lockedVersion).toBeDefined(); + expect(extractedDependencies[1].lockedVersion).toBeUndefined(); + }); + it('handles lock error', () => { + const extractedDependencies = packageJson.extractDependencies( + { dependencies: { chalk: '^2.0.0', foo: '^1.0.0' } }, + 'dependencies', + true + ); + extractedDependencies.should.be.instanceof(Array); + extractedDependencies.should.have.length(2); + expect(extractedDependencies[0].lockedVersion).toBeUndefined(); + expect(extractedDependencies[1].lockedVersion).toBeUndefined(); + }); }); }); diff --git a/test/workers/package-file/index.spec.js b/test/workers/package-file/index.spec.js index 2dabe13e9dff12aeccf0b58e57c5b827ed8e3a95..3af9cd2736b07f858547959f03084a7214137b63 100644 --- a/test/workers/package-file/index.spec.js +++ b/test/workers/package-file/index.spec.js @@ -1,6 +1,9 @@ const packageFileWorker = require('../../../lib/workers/package-file'); const depTypeWorker = require('../../../lib/workers/dep-type'); const defaultConfig = require('../../../lib/config/defaults').getConfig(); +const yarnLock = require('@yarnpkg/lockfile'); + +jest.mock('@yarnpkg/lockfile'); jest.mock('../../../lib/workers/dep-type'); jest.mock('../../../lib/workers/branch/schedule'); @@ -54,6 +57,29 @@ describe('packageFileWorker', () => { const res = await packageFileWorker.renovatePackageFile(config); expect(res).toHaveLength(1); }); + it('skips unparseable yarn.lock', async () => { + config.yarnLock = 'yarn.lock'; + await packageFileWorker.renovatePackageFile(config); + }); + it('skips unparseable yarn.lock', async () => { + config.yarnLock = 'yarn.lock'; + yarnLock.parse.mockReturnValueOnce({ type: 'failure' }); + await packageFileWorker.renovatePackageFile(config); + }); + it('uses workspace yarn.lock', async () => { + config.workspaceDir = '.'; + yarnLock.parse.mockReturnValueOnce({ type: 'success' }); + await packageFileWorker.renovatePackageFile(config); + }); + it('skips unparseable package-lock.json', async () => { + config.packageLock = 'package-lock.lock'; + await packageFileWorker.renovatePackageFile(config); + }); + it('parses package-lock.json', async () => { + config.packageLock = 'package-lock.lock'; + platform.getFile.mockReturnValueOnce('{}'); + await packageFileWorker.renovatePackageFile(config); + }); }); describe('renovateMeteorPackageFile(config)', () => { let config; diff --git a/test/workers/package/__snapshots__/versions.spec.js.snap b/test/workers/package/__snapshots__/versions.spec.js.snap index 7c3ebcf4403c0dd6a40f12499c7a0475a4ba5292..cb279ba2e8151988fbc50dac3146992039f21356 100644 --- a/test/workers/package/__snapshots__/versions.spec.js.snap +++ b/test/workers/package/__snapshots__/versions.spec.js.snap @@ -873,6 +873,28 @@ Array [ ] `; +exports[`workers/package/versions .determineUpgrades(npmDep, config) uses the locked version for pinning 1`] = ` +Array [ + Object { + "changeLogFromVersion": "1.0.0", + "changeLogToVersion": "1.4.1", + "isMinor": true, + "newVersion": "1.4.1", + "newVersionMajor": 1, + "newVersionMinor": 4, + "type": "minor", + "unpublishable": false, + }, + Object { + "isPin": true, + "newVersion": "1.0.0", + "newVersionMajor": 1, + "type": "pin", + "unpublishable": false, + }, +] +`; + exports[`workers/package/versions .determineUpgrades(npmDep, config) widens .x OR ranges 1`] = ` Array [ Object { diff --git a/test/workers/package/versions.spec.js b/test/workers/package/versions.spec.js index c9d9a3df4320617f5ccd349c9157615df47f20df..bd14d4c7dc7db9e45df6ef7efe6f3c5298c9dc51 100644 --- a/test/workers/package/versions.spec.js +++ b/test/workers/package/versions.spec.js @@ -132,6 +132,11 @@ describe('workers/package/versions', () => { config.currentVersion = '^1.0.0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); + it('uses the locked version for pinning', () => { + config.currentVersion = '^1.0.0'; + config.lockedVersion = '1.0.0'; + expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); + }); it('ignores minor ranged versions when not pinning', () => { config.pinVersions = false; config.currentVersion = '^1.0.0'; diff --git a/yarn.lock b/yarn.lock index ddc02697c70f49a8ab552c46291e0beab98c09be..9015dafe41da3458d36fee175897eb397db94b01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -379,6 +379,10 @@ version "2.2.1" resolved "https://registry.yarnpkg.com/@types/write-json-file/-/write-json-file-2.2.1.tgz#74155aaccbb0d532be21f9d66bebc4ea875a5a62" +"@yarnpkg/lockfile@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.0.0.tgz#33d1dbb659a23b81f87f048762b35a446172add3" + "@zkochan/cmd-shim@^2.2.4": version "2.2.4" resolved "https://registry.yarnpkg.com/@zkochan/cmd-shim/-/cmd-shim-2.2.4.tgz#5730a936491219d88487e92d12c6c3bdb16c3c6e"