diff --git a/packages/shipjs/src/helper/__tests__/dependencyUpdater.spec.js b/packages/shipjs/src/helper/__tests__/dependencyUpdater.spec.js new file mode 100644 index 00000000..95070c22 --- /dev/null +++ b/packages/shipjs/src/helper/__tests__/dependencyUpdater.spec.js @@ -0,0 +1,104 @@ +import { getListToUpdate, printListToUpdate } from '../dependencyUpdater'; +import { print } from '../../util'; +import { mockPrint } from '../../../tests/util'; + +describe('getListToUpdate', () => { + const list = [ + { + packagePath: 'packages/package-core', + json: { + name: 'core', + }, + }, + { + packagePath: 'packages/package-js', + json: { + name: 'js', + dependencies: { + core: '^0.3.1', + }, + }, + }, + { + packagePath: 'packages/package-plugin-abc', + json: { + name: 'plugin-abc', + dependencies: { + core: '0.3.1', + }, + peerDependencies: { + js: '~0.3.1', + }, + devDependencies: { + js: '^0.3.1', + }, + }, + }, + ]; + + it('gets correct list', () => { + const actual = getListToUpdate('0.3.2', list); + expect(actual).toEqual([ + { + name: 'js', + packagePath: 'packages/package-js', + updates: { + dependencies: [ + { + currentVersion: '^0.3.1', + dependency: 'core', + nextVersion: '^0.3.2', + }, + ], + }, + }, + { + name: 'plugin-abc', + packagePath: 'packages/package-plugin-abc', + updates: { + dependencies: [ + { + currentVersion: '0.3.1', + dependency: 'core', + nextVersion: '0.3.2', + }, + ], + devDependencies: [ + { + currentVersion: '^0.3.1', + dependency: 'js', + nextVersion: '^0.3.2', + }, + ], + peerDependencies: [ + { + currentVersion: '~0.3.1', + dependency: 'js', + nextVersion: '~0.3.2', + }, + ], + }, + }, + ]); + }); + + it('prints the correct update list', () => { + const output = []; + mockPrint(print, output); + printListToUpdate(getListToUpdate('0.3.2', list)); + expect(output).toMatchInlineSnapshot(` + Array [ + " package: js (packages/package-js}/package.json)", + " dependencies:", + " core: ^0.3.1 -> ^0.3.2", + " package: plugin-abc (packages/package-plugin-abc}/package.json)", + " dependencies:", + " core: 0.3.1 -> 0.3.2", + " devDependencies:", + " js: ^0.3.1 -> ^0.3.2", + " peerDependencies:", + " js: ~0.3.1 -> ~0.3.2", + ] + `); + }); +}); diff --git a/packages/shipjs/src/helper/dependencyUpdater.js b/packages/shipjs/src/helper/dependencyUpdater.js new file mode 100644 index 00000000..048d8023 --- /dev/null +++ b/packages/shipjs/src/helper/dependencyUpdater.js @@ -0,0 +1,119 @@ +import { resolve } from 'path'; +import { readFileSync, writeFileSync } from 'fs'; +import { print } from '../util'; +import { runPrettier } from '../helper'; + +const typesOfDependencies = [ + 'dependencies', + 'devDependencies', + 'peerDependencies', +]; + +export function getListToUpdate(_nextVersion, list) { + const packageNames = list.map(({ json }) => json.name); + + return list + .map(({ packagePath, json }) => { + const updates = typesOfDependencies + .map((dependencyType) => { + let dependenciesToUpdate = Object.keys( + json[dependencyType] || {} + ).filter((dependency) => packageNames.includes(dependency)); + + dependenciesToUpdate = dependenciesToUpdate + .map((dependencyToUpdate) => { + const currentVersion = json[dependencyType][dependencyToUpdate]; + const prefix = + currentVersion.startsWith('~') || currentVersion.startsWith('^') + ? currentVersion[0] + : ''; + const nextVersion = `${prefix}${_nextVersion}`; + if (currentVersion !== nextVersion) { + return { + dependency: dependencyToUpdate, + currentVersion, + nextVersion, + }; + } else { + return null; + } + }) + .filter(Boolean); + + if (dependenciesToUpdate.length === 0) { + return null; + } else { + return { + type: dependencyType, + dependenciesToUpdate, + }; + } + }) + .filter(Boolean); + + if (updates.length === 0) { + return null; + } else { + return { + packagePath, + name: json.name, + updates: updates.reduce((acc, { type, dependenciesToUpdate }) => { + // eslint-disable-next-line no-param-reassign + acc[type] = dependenciesToUpdate; + return acc; + }, {}), + }; + } + }) + .filter(Boolean); +} + +export function printListToUpdate(list) { + list.forEach(({ name, packagePath, updates }) => { + print(` package: ${name} (${packagePath}}/package.json)`); + Object.keys(updates).forEach((dependencyType) => { + print(` ${dependencyType}:`); + updates[dependencyType].forEach( + ({ dependency, currentVersion, nextVersion }) => { + print(` ${dependency}: ${currentVersion} -> ${nextVersion}`); + } + ); + }); + }); +} + +export async function runUpdates(list) { + list.forEach(({ name, packagePath, updates }) => { + print(` package: ${name} (${packagePath}}/package.json)`); + const filePath = resolve(packagePath, 'package.json'); + const json = JSON.parse(readFileSync(filePath).toString()); + Object.keys(updates).forEach((dependencyType) => { + print(` ${dependencyType}:`); + updates[dependencyType].forEach( + ({ dependency, currentVersion, nextVersion }) => { + print(` ${dependency}: ${currentVersion} -> ${nextVersion}`); + json[dependencyType][dependency] = nextVersion; + } + ); + }); + writeFileSync(filePath, JSON.stringify(json, null, 2)); + }); + + await Promise.all( + list.map(({ packagePath }) => + runPrettier({ + filePath: resolve(packagePath, 'package.json'), + dir: packagePath, + }) + ) + ); +} + +export function prepareJsons(packageList) { + return packageList.map((packagePath) => ({ + packagePath, + json: JSON.parse( + readFileSync(resolve(packagePath, 'package.json')).toString() + ), + })); +} diff --git a/packages/shipjs/src/step/prepare/__tests__/updateVersionMonorepo.spec.js b/packages/shipjs/src/step/prepare/__tests__/updateVersionMonorepo.spec.js index 259d53d3..2b53b870 100644 --- a/packages/shipjs/src/step/prepare/__tests__/updateVersionMonorepo.spec.js +++ b/packages/shipjs/src/step/prepare/__tests__/updateVersionMonorepo.spec.js @@ -1,8 +1,23 @@ import { expandPackageList, updateVersion } from 'shipjs-lib'; import { print } from '../../../util'; +import { prepareJsons } from '../../../helper/dependencyUpdater'; + +jest.mock('../../../helper/dependencyUpdater', () => { + return { + ...jest.requireActual('../../../helper/dependencyUpdater'), + prepareJsons: jest.fn(() => []), + }; +}); + import updateVersionMonorepo from '../updateVersionMonorepo'; import { mockPrint } from '../../../../tests/util'; +jest.mock('../updateVersionMonorepo', () => { + const newModule = jest.requireActual('../updateVersionMonorepo'); + newModule.prepareJsons = jest.fn(() => []); + return newModule; +}); + describe('updateVersionMonorepo', () => { it('works', () => { expandPackageList.mockImplementationOnce(() => [ @@ -66,6 +81,7 @@ describe('updateVersionMonorepo', () => { ]); const output = []; mockPrint(print, output); + prepareJsons.mockImplementation(() => []); updateVersionMonorepo({ config: { versionUpdated: () => {}, @@ -84,6 +100,7 @@ describe('updateVersionMonorepo', () => { "Actual packages to bump:", "-> packages/a/package.json", "-> packages/b/package.json", + "Updating dependencies:", "-> execute versionUpdated() callback.", ] `); diff --git a/packages/shipjs/src/step/prepare/updateVersionMonorepo.js b/packages/shipjs/src/step/prepare/updateVersionMonorepo.js index 6099dc99..9df56f74 100644 --- a/packages/shipjs/src/step/prepare/updateVersionMonorepo.js +++ b/packages/shipjs/src/step/prepare/updateVersionMonorepo.js @@ -2,6 +2,12 @@ import { expandPackageList, updateVersion } from 'shipjs-lib'; import runStep from '../runStep'; import { wrapExecWithDir, print } from '../../util'; import { info } from '../../color'; +import { + printListToUpdate, + getListToUpdate, + prepareJsons, + runUpdates, +} from '../../helper/dependencyUpdater'; export default async ({ config, nextVersion, releaseType, dir, dryRun }) => await runStep( @@ -9,9 +15,14 @@ export default async ({ config, nextVersion, releaseType, dir, dryRun }) => async () => { const { versionUpdated, - monorepo: { mainVersionFile, packagesToBump }, + monorepo: { + mainVersionFile, + packagesToBump, + updateDependencies = true, + }, } = config; const packageList = expandPackageList(packagesToBump, dir); + if (dryRun) { print(`Your configuration: ${JSON.stringify(packagesToBump)}`); print(`Main version file: ${mainVersionFile}`); @@ -19,6 +30,16 @@ export default async ({ config, nextVersion, releaseType, dir, dryRun }) => packageList.forEach((packageDir) => print(`-> ${info(`${packageDir}/package.json`)}`) ); + + if (updateDependencies) { + print(`Updating dependencies:`); + printListToUpdate( + getListToUpdate(nextVersion, prepareJsons(packageList)) + ); + } else { + print(`Not updating dependencies.`); + } + if (versionUpdated) { print(`-> execute ${info('versionUpdated()')} callback.`); } @@ -30,6 +51,12 @@ export default async ({ config, nextVersion, releaseType, dir, dryRun }) => print(`-> ${info(`${packageDir}/package.json`)}`); updateVersion({ nextVersion, dir: packageDir }); }); + + if (updateDependencies) { + print(`Updating dependencies:`); + runUpdates(getListToUpdate(nextVersion, prepareJsons(packageList))); + } + if (versionUpdated) { await versionUpdated({ version: nextVersion, diff --git a/ship.config.js b/ship.config.js index f7de4c71..0b422599 100644 --- a/ship.config.js +++ b/ship.config.js @@ -13,16 +13,6 @@ module.exports = { json.version = version; }); - // update package.json - updateJson(dir, "package.json", (json) => { - json.version = version; - }); - - // update dependency - updateJson(dir, "packages/shipjs/package.json", (json) => { - json.dependencies["shipjs-lib"] = version; - }); - // update `version.js` fs.writeFileSync( path.resolve(dir, "packages/shipjs/src/version.js"), diff --git a/website/reference/all-config.md b/website/reference/all-config.md index bde98bd4..92423b71 100644 --- a/website/reference/all-config.md +++ b/website/reference/all-config.md @@ -11,6 +11,7 @@ module.exports = { mainVersionFile: 'package.json', packagesToBump: ['packages/*', 'examples/*'], packagesToPublish: ['packages/*'], + updateDependencies: true // optional, default: true }, }; ``` @@ -21,13 +22,33 @@ If `monorepo` is defined, Ship.js will treat the project as a monorepo. Ship.js currently does not provide independent versioning. It means all the packages in the monorepo must have the same version. ::: -- **`shipjs prepare`** +### **`shipjs prepare`** 1. Ship.js reads version from `mainVersionFile`. 2. When next version is decided, Ship.js will update the version at `mainVersionFile`. 3. Ship.js will update all the versions in `packagesToBump`. +4. When `updateDependencies: true`, it updates the dependencies, too. -- **`shipjs trigger`** +For example, + +```js +// ship.config.js +packagesToBump: ['packages/my-package-core', 'packages/my-package-js'] +``` + +```js +// packages/my-package-js/package.json +{ + ... + "dependencies": { + "my-package-core": "^x.y.z" + } +} +``` + +Ship.js will check `dependencies`, `devDependencies` and `peerDependencies` and update the version to the latest. If you don't want this behavior, put `updateDependencies: false` in the config. + +### **`shipjs trigger`** 1. Ship.js will only publish the packages from `packagesToPublish`.