diff --git a/src/__tests__/publish.test.js b/src/__tests__/publish.test.js index 51a96bb..77b12b5 100755 --- a/src/__tests__/publish.test.js +++ b/src/__tests__/publish.test.js @@ -93,12 +93,6 @@ describe('PUBLISH command', () => { } }); - it('stops when no sub-package is dirty', async () => { - git._setSubpackageDiff(''); - await publish(NOMINAL_OPTIONS); - expect(git.gitPushWithTags).not.toHaveBeenCalled(); - }); - it('proceeds if there are dirty sub-packages', async () => { await publish(NOMINAL_OPTIONS); expect(git.gitPushWithTags).toHaveBeenCalled(); @@ -116,6 +110,203 @@ describe('PUBLISH command', () => { expect(git.gitPushWithTags).toHaveBeenCalledTimes(1); }); + it('updates the version on dirty sub-packages, including exact dependencies', async () => { + const writeSpecs = require('../utils/writeSpecs').default; + const options = Object.assign({}, NOMINAL_OPTIONS, { + bumpDependentReqs: "exact", + }); + await publish(options); + + expect(writeSpecs).toHaveBeenCalledTimes(1 + NUM_FIXTURE_SUBPACKAGES); + writeSpecs.mock.calls.forEach(([, specs]) => { + expect(specs.version).toEqual('99.99.99'); + }); + expect(writeSpecs).toHaveBeenNthCalledWith(1, expect.any(String), expect.objectContaining({ + name: "oao", + version: "99.99.99", + })); + expect(writeSpecs).toHaveBeenNthCalledWith(2, expect.any(String), expect.objectContaining({ + name: "oao-b", + version: "99.99.99", + dependencies: { + oao: "99.99.99", + timm: "1.x", + }, + })); + expect(writeSpecs).toHaveBeenNthCalledWith(3, expect.any(String), expect.objectContaining({ + name: "oao-c", + version: "99.99.99", + dependencies: { + oao: "99.99.99", + timm: "1.x", + }, + devDependencies: { + "oao-b": "99.99.99", + "xxl": "1.x", + }, + })); + expect(writeSpecs).toHaveBeenNthCalledWith(4, expect.any(String), expect.objectContaining({ + name: "oao-d", + version: "99.99.99", + dependencies: { + oao: "99.99.99", + timm: "1.x", + }, + devDependencies: { + "oao-b": "99.99.99", + "oao-c": "99.99.99", + "xxl": "1.x", + }, + })); + expect(writeSpecs).toHaveBeenNthCalledWith(5, expect.any(String), expect.objectContaining({ + name: "oao-priv", + version: "99.99.99", + dependencies: { + oao: "99.99.99", + timm: "1.x", + }, + devDependencies: { + "oao-b": "99.99.99", + "oao-c": "99.99.99", + "xxl": "1.x", + }, + })); + expect(git.gitCommitChanges).toHaveBeenCalledTimes(1); + expect(git.gitAddTag).toHaveBeenCalledTimes(1); + expect(git.gitPushWithTags).toHaveBeenCalledTimes(1); + }); + + it('updates the version on dirty sub-packages, including dependencies in range', async () => { + const writeSpecs = require('../utils/writeSpecs').default; + await publish(NOMINAL_OPTIONS); + + expect(writeSpecs).toHaveBeenCalledTimes(1 + NUM_FIXTURE_SUBPACKAGES); + + writeSpecs.mock.calls.forEach(([, specs]) => { + expect(specs.version).toEqual('99.99.99'); + }); + expect(writeSpecs).toHaveBeenNthCalledWith(1, expect.any(String), expect.objectContaining({ + name: "oao", + version: "99.99.99", + })); + expect(writeSpecs).toHaveBeenNthCalledWith(2, expect.any(String), expect.objectContaining({ + name: "oao-b", + version: "99.99.99", + dependencies: { + oao: "^99.99.99", + timm: "1.x", + }, + })); + expect(writeSpecs).toHaveBeenNthCalledWith(3, expect.any(String), expect.objectContaining({ + name: "oao-c", + version: "99.99.99", + dependencies: { + oao: "^99.99.99", + timm: "1.x", + }, + devDependencies: { + "oao-b": "^99.99.99", + "xxl": "1.x", + }, + })); + expect(writeSpecs).toHaveBeenNthCalledWith(4, expect.any(String), expect.objectContaining({ + name: "oao-d", + version: "99.99.99", + dependencies: { + oao: "^99.99.99", + timm: "1.x", + }, + devDependencies: { + "oao-b": "^99.99.99", + "oao-c": "^99.99.99", + "xxl": "1.x", + }, + })); + expect(writeSpecs).toHaveBeenNthCalledWith(5, expect.any(String), expect.objectContaining({ + name: "oao-priv", + version: "99.99.99", + dependencies: { + oao: "^99.99.99", + timm: "1.x", + }, + devDependencies: { + "oao-b": "^99.99.99", + "oao-c": "^99.99.99", + "xxl": "1.x", + }, + })); + expect(git.gitCommitChanges).toHaveBeenCalledTimes(1); + expect(git.gitAddTag).toHaveBeenCalledTimes(1); + expect(git.gitPushWithTags).toHaveBeenCalledTimes(1); + }); + + it('updates the version on dirty sub-packages, without dependencies', async () => { + const writeSpecs = require('../utils/writeSpecs').default; + const options = Object.assign({}, NOMINAL_OPTIONS, { + bumpDependentReqs: "no", + }); + await publish(options); + + expect(writeSpecs).toHaveBeenCalledTimes(1 + NUM_FIXTURE_SUBPACKAGES); + + writeSpecs.mock.calls.forEach(([, specs]) => { + expect(specs.version).toEqual('99.99.99'); + }); + expect(writeSpecs).toHaveBeenNthCalledWith(1, expect.any(String), expect.objectContaining({ + name: "oao", + version: "99.99.99", + })); + expect(writeSpecs).toHaveBeenNthCalledWith(2, expect.any(String), expect.objectContaining({ + name: "oao-b", + version: "99.99.99", + dependencies: { + oao: "*", + timm: "1.x", + }, + })); + expect(writeSpecs).toHaveBeenNthCalledWith(3, expect.any(String), expect.objectContaining({ + name: "oao-c", + version: "99.99.99", + dependencies: { + oao: "*", + timm: "1.x", + }, + devDependencies: { + "oao-b": "*", + "xxl": "1.x", + }, + })); + expect(writeSpecs).toHaveBeenNthCalledWith(4, expect.any(String), expect.objectContaining({ + name: "oao-d", + version: "99.99.99", + dependencies: { + oao: "*", + timm: "1.x", + }, + devDependencies: { + "oao-b": "*", + "oao-c": "*", + "xxl": "1.x", + }, + })); + expect(writeSpecs).toHaveBeenNthCalledWith(5, expect.any(String), expect.objectContaining({ + name: "oao-priv", + version: "99.99.99", + dependencies: { + oao: "*", + timm: "1.x", + }, + devDependencies: { + "oao-b": "*", + "oao-c": "*", + "xxl": "1.x", + }, + })); + expect(git.gitCommitChanges).toHaveBeenCalledTimes(1); + expect(git.gitAddTag).toHaveBeenCalledTimes(1); + expect(git.gitPushWithTags).toHaveBeenCalledTimes(1); + }); + it('increments version by major when incrementVersionBy is "major" and newVersion is not set', async () => { const writeSpecs = require('../utils/writeSpecs').default; const options = Object.assign({}, NOMINAL_OPTIONS, { @@ -196,6 +387,10 @@ describe('PUBLISH command', () => { it('runs `npm publish` on dirty sub-packages (which are not private)', async () => { const { exec } = require('../utils/shell'); + git.gitDiffSinceIn.mockImplementation((version, path) => { + // Only oao-d was modified, but everything is still published + return path === "test/fixtures/packages/oao-d" ? "SOMETHING_HAS_CHANGED" : ""; + }) await publish(NOMINAL_OPTIONS); expect(exec).toHaveBeenCalledTimes( NUM_FIXTURE_SUBPACKAGES - NUM_FIXTURE_PRIVATE_SUBPACKAGES @@ -224,6 +419,22 @@ describe('PUBLISH command', () => { ]); }); + it('runs `npm publish` on dirty sub-packages, with dependencies (which are not private)', async () => { + const { exec } = require('../utils/shell'); + + git.gitDiffSinceIn.mockImplementation((version, path) => { + // Only oao-b was modified, but everything is still published + return path === "test/fixtures/packages/oao-b" ? "SOMETHING_HAS_CHANGED" : ""; + }) + await publish(NOMINAL_OPTIONS); + expect(exec).toHaveBeenCalledTimes( + NUM_FIXTURE_SUBPACKAGES - NUM_FIXTURE_PRIVATE_SUBPACKAGES + ); + exec.mock.calls.forEach(([cmd]) => { + expect(cmd).toEqual('npm publish'); + }); + }); + it('runs `npm publish --tag X` on dirty sub-packages', async () => { const { exec } = require('../utils/shell'); await publish(merge(NOMINAL_OPTIONS, { publishTag: 'next' })); diff --git a/src/index.js b/src/index.js index d274dfa..4a69f6f 100755 --- a/src/index.js +++ b/src/index.js @@ -242,6 +242,7 @@ createCommand('publish', 'Publish updated sub-packages') .option('--no-changelog', 'skip changelog updates') .option('--otp ', 'use 2FA to publish your package') .option('--access ', 'publish public or restricted packages') + .option('--bump-dependent-reqs ', 'bump dependencies ') .action(cmd => { const options = processOptions(cmd.opts()); initConsole(options); diff --git a/src/publish.js b/src/publish.js index b27421f..3634b29 100755 --- a/src/publish.js +++ b/src/publish.js @@ -4,6 +4,7 @@ import semver from 'semver'; import inquirer from 'inquirer'; import { mainStory, chalk } from 'storyboard'; import { readAllSpecs, ROOT_PACKAGE } from './utils/readSpecs'; +import { DEP_TYPES } from './utils/constants'; import writeSpecs from './utils/writeSpecs'; import { exec } from './utils/shell'; import { @@ -35,6 +36,7 @@ type Options = { checks: boolean, confirm: boolean, bump: boolean, + bumpDependentReqs: "range" | "exact" | "no", gitCommit: boolean, newVersion?: string, npmPublish: boolean, @@ -58,6 +60,7 @@ const run = async ({ checks, confirm, bump, + bumpDependentReqs, gitCommit, newVersion, npmPublish, @@ -82,8 +85,8 @@ const run = async ({ // Get last tag and find packages requiring updates // (modified since the last tag) const lastTag = await gitLastTag(); - const dirty = await findPackagesToUpdate(allSpecs, lastTag, single); - if (!dirty.length) { + const toUpdate = await findPackagesToUpdate(allSpecs, lastTag, single); + if (!toUpdate.length) { mainStory.info('No packages have been updated'); return; } @@ -100,14 +103,28 @@ const run = async ({ (await promptNextVersion(masterVersion)); // Confirm before proceeding - if (confirm && !(await confirmPublish({ dirty, nextVersion }))) return; + if (confirm && !(await confirmPublish({ toUpdate, nextVersion }))) return; - // Update package.json's for dirty packages AND THE ROOT PACKAGE + changelog - const dirtyPlusRoot = single ? dirty : dirty.concat(ROOT_PACKAGE); - for (let i = 0; i < dirtyPlusRoot.length; i++) { - const pkgName = dirtyPlusRoot[i]; + const nextVersionForReqs = getDependencyReqVersion(bumpDependentReqs, nextVersion) + + // Update package.json's for packages to update AND THE ROOT PACKAGE + changelog + const toUpdatePlusRoot = single ? toUpdate : toUpdate.concat(ROOT_PACKAGE); + for (let i = 0; i < toUpdatePlusRoot.length; i++) { + const pkgName = toUpdatePlusRoot[i]; const { specPath, specs } = allSpecs[pkgName]; specs.version = nextVersion; + + // Also update dependencies to package we'll publish + if (nextVersionForReqs) { + toUpdate.forEach(pkgNameToUpdate => { + DEP_TYPES.forEach(type => { + if (specs[type] != null && specs[type][pkgNameToUpdate] != null) { + specs[type][pkgNameToUpdate] = nextVersionForReqs; + } + }); + }); + } + writeSpecs(specPath, specs); } if (changelog) @@ -123,8 +140,8 @@ const run = async ({ // Publish if (npmPublish) { - for (let i = 0; i < dirty.length; i++) { - const pkgName = dirty[i]; + for (let i = 0; i < toUpdate.length; i++) { + const pkgName = toUpdate[i]; const { pkgPath, specs } = allSpecs[pkgName]; if (specs.private) continue; // we don't want npm to complain :) let cmd = 'npm publish'; @@ -210,13 +227,13 @@ const confirmBuild = async () => { return out; }; -const confirmPublish = async ({ dirty, nextVersion }) => { +const confirmPublish = async ({ toUpdate, nextVersion }) => { const { confirmPublish: out } = await inquirer.prompt([ { name: 'confirmPublish', type: 'confirm', message: - `Confirm release (${chalk.yellow.bold(dirty.length)} package/s, ` + + `Confirm release (${chalk.yellow.bold(toUpdate.length)} package/s, ` + `v${chalk.cyan.bold(nextVersion)})?`, default: false, }, @@ -240,23 +257,30 @@ const validateVersionIncrement = incrementVersionBy => { const findPackagesToUpdate = async (allSpecs, lastTag, single) => { const pkgNames = Object.keys(allSpecs); - const dirty = []; + const toUpdate = []; + + // Collect changed packages for (let i = 0; i < pkgNames.length; i++) { const pkgName = pkgNames[i]; if (pkgName === ROOT_PACKAGE && !single) continue; const { pkgPath, specs } = allSpecs[pkgName]; const diff = await gitDiffSinceIn(lastTag, pkgPath); - if (diff !== '') { - const numChanges = diff.split('\n').length; - mainStory.info( - `- Package ${pkgName} (currently ${chalk.cyan.bold( - specs.version - )}) has changed (#files: ${numChanges})` - ); - dirty.push(pkgName); - } + const files = diff.split('\n').length; + + mainStory.info( + `- Package ${pkgName} (currently ${chalk.cyan.bold( + specs.version + )})${ + files > 0 + ? ` has changed (#files: ${files})` + : '' + }` + ); + + toUpdate.push(pkgName); } - return dirty; + + return toUpdate; }; const getMasterVersion = async (allSpecs, lastTag) => { @@ -308,6 +332,14 @@ const getMasterVersion = async (allSpecs, lastTag) => { return masterVersion; }; +const getDependencyReqVersion = (bumpDependentReqs, nextVersion) => { + if (bumpDependentReqs === "no") { + return null; + } + + return bumpDependentReqs === 'exact' ? nextVersion : `^${nextVersion}`; +} + const calcNextVersion = (prevVersion: string, incrementBy = ''): string => { const isPreRelease = PRERELEASE_INCREMENTS.indexOf(incrementBy) >= 0; const increment = isPreRelease ? 'prerelease' : incrementBy;