diff --git a/lib/index.js b/lib/index.js index 5565ef3..af14b4b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -21,10 +21,24 @@ async function getLatestVersions(name) { } } +async function getLatestTag(name) { + const { stdout } = await execAsync(`npm view ${name} dist-tags --json`); + try { + return JSON.parse(stdout); + } catch (err) { + throw new Error(`Failed to parse output from NPM view - ${err.toString()}`); + } +} + async function getLatestVersion(name, wanted) { const versions = await getLatestVersions(name); + const { latest } = await getLatestTag(name); const applicableVersions = versions.filter(i => semver.satisfies(i, wanted)); applicableVersions.sort((a, b) => semver.rcompare(a, b)); + + if (latest && semver.lt(latest, applicableVersions[0])) { + return latest; + } return applicableVersions[0]; } diff --git a/lib/index.test.js b/lib/index.test.js index 4adc9e9..464fa62 100644 --- a/lib/index.test.js +++ b/lib/index.test.js @@ -19,7 +19,8 @@ jest.mock('chalk', () => ({ })); const chance = new Chance(); -const moduleNameRegex = new RegExp('npm view (.*) versions --json'); +const moduleNameRegexVersions = new RegExp('npm view (.*) versions --json'); +const moduleNameRegexTags = new RegExp('npm view (.*) dist-tags --json'); const pathString = './index.test.js'; const mockExecAsync = getMockExecAsync(); const mockExports = {}; @@ -34,7 +35,8 @@ describe('lib/index', () => { let olderVersion; let newerVersion; const mock = command => { - const moduleName = command.match(moduleNameRegex)[1]; + const result = command.match(moduleNameRegexVersions) || command.match(moduleNameRegexTags); + const moduleName = result[1]; const versions = [olderVersion]; if (moduleName === outdatedDep || moduleName === outdatedDevDep) versions.push(newerVersion); return Promise.resolve({ stdout: JSON.stringify(versions) }); @@ -185,6 +187,25 @@ describe('lib/index', () => { ); }); + it('should show dependency install required if fetching tags does not return valid JSON output', async () => { + const invalidOutput = chance.word(); + mockExecAsync + .mockImplementationOnce(() => ({ stdout: JSON.stringify(['1.1.1']) })) + .mockImplementationOnce(() => ({ stdout: JSON.stringify(['1.1.1']) })) + .mockImplementationOnce(() => ({ stdout: JSON.stringify(['1.1.1']) })) + .mockImplementationOnce(() => ({ stdout: JSON.stringify(['1.1.1']) })); + mockExecAsync.mockImplementation(() => ({ stdout: invalidOutput })); + let syntaxError; + try { + JSON.parse(invalidOutput); + } catch (err) { + syntaxError = err; + } + await expect(verifyDeps({ dir, logger })).rejects.toThrow( + `Failed to parse output from NPM view - ${syntaxError.toString()}` + ); + }); + it('should show dependency install required if latest module is installed but not reflected in package.json', async () => { mockExports.dependencies[outdatedDep] = `^${newerVersion}`; mockExports.devDependencies[outdatedDevDep] = `^${newerVersion}`; @@ -257,6 +278,34 @@ describe('lib/index', () => { console.info = consoleInfo; }); + it('should update to version aliased as latest when aliased latest is less that most recent published version', async () => { + mockExports.dependencies = { foo1: '1.2.3' }; + mockExports.devDependencies = { fooDev1: '1.2.3' }; + + mockExecAsync + .mockImplementationOnce(() => Promise.resolve({ stdout: JSON.stringify(['1.2.4', '1.2.5']) })) + .mockImplementationOnce(() => Promise.resolve({ stdout: JSON.stringify(['1.2.4', '1.2.5']) })) + .mockImplementationOnce(() => + Promise.resolve({ stdout: JSON.stringify({ latest: '1.2.4' }) }) + ) + .mockImplementationOnce(() => + Promise.resolve({ stdout: JSON.stringify({ latest: '1.2.4' }) }) + ) + .mockImplementationOnce(() => Promise.resolve({ stdout: JSON.stringify(['1.2.4']) })) + .mockImplementationOnce(() => Promise.resolve({ stdout: JSON.stringify(['1.2.4']) })); + + await verifyDeps({ autoUpgrade: true, dir, logger }); + + expect(logger.info).toHaveBeenCalledTimes(7); + expect(logger.info).toHaveBeenNthCalledWith(1, 'Verifying dependencies…\n'); + expect(logger.info).toHaveBeenNthCalledWith(2, `foo1 is outdated: 1.2.3 → 1.2.4`); + expect(logger.info).toHaveBeenNthCalledWith(3, `fooDev1 is outdated: 1.2.3 → 1.2.4`); + expect(logger.info).toHaveBeenNthCalledWith(4, 'UPGRADING…'); + expect(logger.info).toHaveBeenNthCalledWith(5, `npm i foo1@1.2.4 \nnpm i -D fooDev1@1.2.4 `); + expect(logger.info).toHaveBeenNthCalledWith(6, `Upgraded dependencies:\n["1.2.4"]`); + expect(logger.info).toHaveBeenNthCalledWith(7, `Upgraded development dependencies:\n["1.2.4"]`); + }); + test('autoUpgrade modules', async () => { const mock2 = command => { const moduleName = command.match('npm i (.*)')[1]; @@ -266,6 +315,10 @@ describe('lib/index', () => { }; mockExecAsync + .mockImplementationOnce(mock) + .mockImplementationOnce(mock) + .mockImplementationOnce(mock) + .mockImplementationOnce(mock) .mockImplementationOnce(mock) .mockImplementationOnce(mock) .mockImplementationOnce(mock)