Skip to content

Commit 7d5848a

Browse files
committed
feat(sync): synchronise versions across multiple package.json
1 parent cf0c22c commit 7d5848a

8 files changed

Lines changed: 186 additions & 13 deletions

File tree

.babelrc

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
{
22
"presets": ["es2015"],
33
"plugins": [
4-
[
5-
"fast-async",
6-
{
7-
"useRuntimeModule": true
8-
}
9-
]
4+
["fast-async", { "useRuntimeModule": true }],
5+
"transform-object-rest-spread"
106
]
117
}

README.md

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,45 @@
77
[![Join the chat at https://gitter.im/JamieMason/syncpack](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/JamieMason/syncpack)
88
[![Analytics](https://ga-beacon.appspot.com/UA-45466560-5/syncpack?flat&useReferer)](https://github.com/igrigorik/ga-beacon)
99

10-
Normalise differences across multiple `package.json` files, such as `packages/*/package.json` in [Lerna](https://lernajs.io) Monorepos.
10+
Synchronises the versions of dependencies used across multiple `package.json` files, such as
11+
`packages/*/package.json` in [Lerna](https://lernajs.io) Monorepos.
1112

12-
## Status
13+
## Overview
1314

14-
In Development, not yet ready for use.
15+
Imagine the packages `guybrush`, `herman`, and `elaine` all have `react` as a dependency, but
16+
versions `'15.4.0'`, `'15.5.4'`, and `'15.6.1'` respectively.
17+
18+
```
19+
/Users/foldleft/Dev/monorepo/packages/
20+
├── guybrush
21+
│   └── package.json
22+
├── herman
23+
│   └── package.json
24+
└── elaine
25+
└── package.json
26+
```
27+
28+
Running `syncpack` will update each `package.json` to use version `'15.6.1'` of `react` in
29+
`dependencies`, `devDependencies`, and `peerDependencies` as needed.
30+
31+
## Installation
32+
33+
```
34+
npm install --global syncpack
35+
```
36+
37+
## Usage
38+
39+
### Command Line
40+
41+
```
42+
Usage: syncpack [options] [pattern]
43+
44+
Options:
45+
46+
-h, --help output usage information
47+
-V, --version output the version number
48+
```
49+
50+
The default pattern of `'./packages/*/package.json'` can be overridden as follows
51+
`syncpack './**/package.json'`

package-lock.json

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
},
1818
"devDependencies": {
1919
"babel-cli": "6.24.1",
20+
"babel-plugin-transform-object-rest-spread": "6.26.0",
2021
"babel-preset-es2015": "6.22.0",
2122
"fast-async": "6.3.0",
2223
"rimraf": "2.5.4",

src/bin.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@ import { version } from '../package.json';
66
import * as log from './lib/log';
77
import syncpack from './index';
88

9-
program.version(version);
9+
let patternValue;
10+
11+
program.version(version).arguments('[pattern]').action(pattern => {
12+
patternValue = pattern;
13+
});
14+
1015
program.parse(process.argv);
1116

12-
syncpack({}).catch(err => {
17+
syncpack({
18+
pattern: patternValue
19+
}).catch(err => {
1320
log.bug('uncaught error in syncpack', err);
1421
process.exit(1);
1522
});

src/index.js

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,96 @@
1-
export default () => Promise.resolve();
1+
import bluebird from 'bluebird';
2+
import chalk from 'chalk';
3+
import path from 'path';
4+
import semver from 'semver';
5+
import * as log from './lib/log';
6+
import getFiles from './lib/get-files';
7+
import writeJson from './lib/write-json';
8+
9+
const keys = (object = {}) => Object.keys(object);
10+
const clone = object => JSON.parse(JSON.stringify(object));
11+
const concatAll = arrayOfArrays => [].concat.apply([], arrayOfArrays);
12+
const entries = object => keys(object).map(key => [key, object[key]]);
13+
const isEmptyObject = object => keys(object).length === 0;
14+
const pluck = key => object => object[key];
15+
16+
const stripWildCards = version => version.replace(/[*^=><]/g, '');
17+
const getPackage = location => ({ location: location, json: require(location) });
18+
const takeNewest = (max, next) =>
19+
!semver.valid(stripWildCards(next)) || semver.gt(stripWildCards(next), stripWildCards(max))
20+
? next
21+
: max;
22+
23+
const indexEntries = array =>
24+
array.reduce((index, [key, value]) => {
25+
index[key] = index[key] || [];
26+
index[key] = index[key].indexOf(value) === -1 ? index[key].concat(value) : index[key];
27+
return index;
28+
}, {});
29+
30+
const getNewestDeps = (key, packages) => {
31+
const dependencies = concatAll(packages.map(pluck('json')).map(pluck(key)).map(entries));
32+
const versionsByDependencyName = indexEntries(dependencies);
33+
return entries(versionsByDependencyName).map(([name, versions]) => {
34+
const newest = versions.reduce(takeNewest, '0.0.0');
35+
return [name, newest];
36+
});
37+
};
38+
39+
const getChangedDeps = (key, packages) =>
40+
packages.map(pkg =>
41+
getNewestDeps(key, packages).reduce((changes, [name, version]) => {
42+
if (pkg.json[key] && name in pkg.json[key] && pkg.json[key][name] !== version) {
43+
changes[name] = version;
44+
}
45+
return changes;
46+
}, {})
47+
);
48+
49+
const reportChanges = (key, pkg, changes) => {
50+
const changedEntries = entries(changes);
51+
if (changedEntries.length) {
52+
changedEntries.forEach(([name, version]) => {
53+
console.log(`${key} ${name} ${chalk.red(pkg.json[key][name])}${chalk.green(version)}`);
54+
});
55+
} else {
56+
console.log(`${key} ${chalk.green('✓ unchanged')}`);
57+
}
58+
};
59+
60+
export default async ({ pattern = './packages/*/package.json' }) => {
61+
const packages = (await getFiles(pattern)).map(getPackage);
62+
const changedDeps = getChangedDeps('dependencies', packages);
63+
const changedDevDeps = getChangedDeps('devDependencies', packages);
64+
const changedPeerDeps = getChangedDeps('peerDependencies', packages);
65+
66+
const nextPackages = packages.map(({ location, json }, i) => {
67+
const nextPkg = {
68+
location,
69+
json: {
70+
...json,
71+
dependencies: { ...json.dependencies, ...changedDeps[i] },
72+
devDependencies: { ...json.devDependencies, ...changedDevDeps[i] },
73+
peerDependencies: { ...json.peerDependencies, ...changedPeerDeps[i] }
74+
}
75+
};
76+
if (isEmptyObject(nextPkg.json.dependencies)) {
77+
delete nextPkg.json.dependencies;
78+
}
79+
if (isEmptyObject(nextPkg.json.devDependencies)) {
80+
delete nextPkg.json.devDependencies;
81+
}
82+
if (isEmptyObject(nextPkg.json.peerDependencies)) {
83+
delete nextPkg.json.peerDependencies;
84+
}
85+
return nextPkg;
86+
});
87+
88+
packages.forEach((pkg, i) => {
89+
console.log(chalk.grey.underline(path.relative(process.cwd(), pkg.location)));
90+
reportChanges('dependencies', pkg, changedDeps[i]);
91+
reportChanges('devDependencies', pkg, changedDevDeps[i]);
92+
reportChanges('peerDependencies', pkg, changedPeerDeps[i]);
93+
});
94+
95+
await bluebird.all(nextPackages.map(pkg => writeJson(pkg.location, pkg.json)));
96+
};

src/lib/get-files.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import glob from 'glob';
2+
import * as log from './log';
3+
4+
export default pattern =>
5+
new Promise(resolve =>
6+
glob(pattern, { absolute: true }, (err, files) => {
7+
if (err) {
8+
log.bug(`failed to search for files using pattern "${pattern}"`, err);
9+
process.exit(1);
10+
}
11+
resolve(files);
12+
})
13+
);

src/lib/write-json.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import bluebird from 'bluebird';
2+
import fs from 'fs';
3+
4+
export default async (location, contents) => {
5+
const json = JSON.stringify(contents, null, 2);
6+
const writeFile = bluebird.promisify(fs.writeFile);
7+
return await writeFile(location, `${json}\n`, { encoding: 'utf8' });
8+
};

0 commit comments

Comments
 (0)