├── .gitignore
├── .npmignore
├── README.md
├── cli.js
├── media
├── demo3.png
├── demo4.png
├── demo5.png
├── demo6.png
├── demo7.png
└── demo8.png
├── package.json
└── src
├── __test__
├── combinator.test.js
└── entropy_service.test.js
├── combinator.js
├── detective.js
├── entropy_service.js
├── index.js
├── logger.js
└── utils
├── __test__
└── sorters.test.js
└── sorters.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | install_nvm.sh
4 | node_modules
5 | references
6 | webpack
7 | packages.cache.json
8 | npm-debug.log
9 | .npmrc
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | /node_modules
3 | npm-debug.log
4 | .idea
5 | .DS_Store
6 | .npmrc
7 | /media
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ostap
2 |
3 | CLI tool that fast checks if your bundle contains multiple versions of the same package, only by looking in package.json.
4 |
5 | Advantages:
6 |
7 | * faster than alternatives, since it doesn't require rebuilding the bundle (example, [duplicate-package-checker-webpack-plugin](https://github.com/darrenscerri/duplicate-package-checker-webpack-plugin));
8 |
9 | * uses only package.json;
10 |
11 | * suggests optimal package versions; [see how](#for-suggests-optimal-package-versions)
12 |
13 | * you can quickly see all the current versions of the same package that are used in the current bundle. [see how](#for-see-all-the-current-versions-of-the-same-package-that-are-used-in-the-current-bundle)
14 |
15 | ## Quick start
16 | ```
17 | # create package.json if not exists
18 | echo "{\"name\":\"demo-project\",\"version\":\"1.0.0\",\"dependencies\":{\"@nivo/bar\":\"0.54.0\",\"@nivo/core\":\"0.53.0\",\"@nivo/pie\":\"0.54.0\",\"@nivo/stream\":\"0.54.0\"}}" > ./package.json
19 |
20 | npx ostap ./package.json -s
21 | ```
22 |
23 | ## How to use
24 |
25 | For example, you have `package.json`:
26 | ```
27 | {
28 | "name": "demo-project",
29 | "version": "1.0.0",
30 | "dependencies": {
31 | "@nivo/bar": "0.54.0",
32 | "@nivo/core": "0.53.0",
33 | "@nivo/pie": "0.54.0",
34 | "@nivo/stream": "0.54.0"
35 | }
36 | }
37 | ```
38 | ### For suggests optimal package versions
39 |
40 | ```
41 | ostap ./package.json
42 | ```
43 |
44 |
45 | ### For see all the current versions of the same package that are used in the current bundle
46 |
47 | ```
48 | ostap ./package.json -s
49 | ```
50 |
51 |
52 | ### Installation
53 | ```
54 | npm i -g ostap
55 | ```
56 | ### Options
57 | ```
58 | Options:
59 | -c, --use-cache Use cache
60 | -d, --duplicates Show duplicates in source and optimal tree
61 | -s, --source-tree-duplicates Show duplicates in source tree
62 | -o, --optimal-tree-duplicates Show duplicates in optimal tree
63 | -v, --version Display version number
64 | -h, --help Display help
65 | ```
66 | ## Contributing
67 | Got ideas on how to make this better? Open an issue!
68 |
69 | ## License
70 | MIT
71 |
72 | ## Who is Ostap?
73 | Ostap Bender is a fictional man who appeared in the novels The Twelve Chairs and The Little Golden Calf written by Soviet authors Ilya Ilf and Yevgeni Petrov. His description as "The Great Combinator" became a catch phrase in the Russian language.
74 |
--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict';
4 |
5 | const cac = require('cac');
6 | const fs = require('fs');
7 | const chalk = require('chalk');
8 | const ostap = require('./src');
9 |
10 | const Confirm = require('prompt-confirm');
11 |
12 | const pkg = require('./package.json');
13 |
14 | const DEFAULT_OPTIONS = {
15 | useCache: false,
16 | viewFullLogs: false,
17 | printSourceTreeDuplicates: false,
18 | printOptimalTreeDuplicates: false,
19 | };
20 |
21 | const run = async (file, options) => {
22 | try {
23 | await fs.accessSync(file, fs.constants.R_OK);
24 | } catch (error) {
25 | throw 'no access for read, file: ' + file;
26 | }
27 |
28 | let treeRoot = {};
29 |
30 | try {
31 | treeRoot = JSON.parse(fs.readFileSync(file));
32 | } catch (error) {
33 | throw 'parse error file ' + file + '\n' + error;
34 | }
35 |
36 | let optimalTree = null;
37 | try {
38 | optimalTree = await ostap(treeRoot, options);
39 | } catch (error) {
40 | throw error;
41 | }
42 |
43 | if (optimalTree) {
44 | const prompt = new Confirm({
45 | message: 'Apply suggested update to original package.json file?',
46 | default: false,
47 | });
48 |
49 | const answer = await prompt.run();
50 |
51 | if (answer) {
52 | try {
53 | await fs.accessSync(file, fs.constants.W_OK);
54 | } catch (error) {
55 | throw 'no access for write, file: ' + file;
56 | }
57 |
58 | try {
59 | fs.writeFileSync(file, JSON.stringify(optimalTree, null, 4));
60 | } catch (error) {
61 | throw 'write error' + file + '\n' + error;
62 | }
63 | console.log('changed package.json');
64 | }
65 | }
66 |
67 | console.log('bye');
68 | };
69 |
70 | const printError = message => {
71 | console.log(chalk.bgRed.black(' ERROR '));
72 | console.log(message);
73 | };
74 |
75 | const cli = cac();
76 |
77 | cli
78 | .command('[...file]', 'Check project dependencies duplicates by package.json')
79 | .option('-c, --use-cache', 'Use cache')
80 | .option('-d, --duplicates', 'Show duplicates in source and optimal tree')
81 | .option('-s, --source-tree-duplicates', 'Show duplicates in source tree')
82 | .option('-o, --optimal-tree-duplicates', 'Show duplicates in optimal tree')
83 | .example('ostap ./package.json')
84 | .example('ostap ./package.json --use-cache')
85 | .example('ostap /Users/frontend/monkey/package.json -d')
86 | .action(([filePath], flags) => {
87 | const file = filePath || './package.json';
88 | let options = { ...DEFAULT_OPTIONS };
89 |
90 | if (flags['useCache']) {
91 | options.useCache = true;
92 | }
93 |
94 | if (flags['sourceTreeDuplicates'] || flags['duplicates']) {
95 | options.printSourceTreeDuplicates = true;
96 | }
97 |
98 | if (flags['optimalTreeDuplicates'] || flags['duplicates']) {
99 | options.printOptimalTreeDuplicates = true;
100 | }
101 |
102 | Promise.resolve()
103 | .then(() => run(file, options))
104 | .catch(printError);
105 | });
106 |
107 | cli.version(pkg.version);
108 | cli.help();
109 |
110 | cli.parse();
111 |
--------------------------------------------------------------------------------
/media/demo3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itwillwork/ostap/b3c844169332adafe68576605459c2b44925bdf9/media/demo3.png
--------------------------------------------------------------------------------
/media/demo4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itwillwork/ostap/b3c844169332adafe68576605459c2b44925bdf9/media/demo4.png
--------------------------------------------------------------------------------
/media/demo5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itwillwork/ostap/b3c844169332adafe68576605459c2b44925bdf9/media/demo5.png
--------------------------------------------------------------------------------
/media/demo6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itwillwork/ostap/b3c844169332adafe68576605459c2b44925bdf9/media/demo6.png
--------------------------------------------------------------------------------
/media/demo7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itwillwork/ostap/b3c844169332adafe68576605459c2b44925bdf9/media/demo7.png
--------------------------------------------------------------------------------
/media/demo8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itwillwork/ostap/b3c844169332adafe68576605459c2b44925bdf9/media/demo8.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ostap",
3 | "version": "1.1.3",
4 | "description": "",
5 | "main": "./src/index.js",
6 | "scripts": {
7 | "format": "prettier --single-quote --trailing-comma es5 --write \"src/**/*.js\"",
8 | "test": "jest"
9 | },
10 | "bin": "cli.js",
11 | "files": [
12 | "src",
13 | "cli.js"
14 | ],
15 | "engines": {
16 | "node": ">=8.4.0"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/itwillwork/ostap.git"
21 | },
22 | "author": "",
23 | "license": "ISC",
24 | "bugs": {
25 | "url": "https://github.com/itwillwork/ostap/issues"
26 | },
27 | "homepage": "https://github.com/itwillwork/ostap#readme",
28 | "dependencies": {
29 | "cac": "6.4.2",
30 | "chalk": "2.4.2",
31 | "dependencies-tree-builder": "1.0.5",
32 | "lodash": "4.17.13",
33 | "ora": "3.2.0",
34 | "prompt-confirm": "2.0.4",
35 | "semver": "5.6.0"
36 | },
37 | "devDependencies": {
38 | "jest": "24.3.1"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/__test__/combinator.test.js:
--------------------------------------------------------------------------------
1 | const Combinator = require('../combinator');
2 |
3 | test('getSimpleVariant', async () => {
4 | // Arrange
5 | const fakeSuspects = [{ name: 'a', version: 1 }, { name: 'b', version: 2 }];
6 |
7 | // Act
8 | const combinator = new Combinator();
9 | const result = combinator.getSimpleVariant(fakeSuspects);
10 |
11 | // Assert
12 | expect(result).toEqual([
13 | {
14 | dependenciesChanges: {
15 | a: 1,
16 | },
17 | },
18 | {
19 | dependenciesChanges: {
20 | b: 2,
21 | },
22 | },
23 | ]);
24 | });
25 |
26 | test('getComplexVariant', async () => {
27 | // Arrange
28 | const suspects = [
29 | {
30 | dependenciesChanges: {
31 | c: 1,
32 | },
33 | entropy: 3,
34 | },
35 | {
36 | dependenciesChanges: {
37 | a: 1,
38 | },
39 | entropy: 2,
40 | },
41 | {
42 | dependenciesChanges: {
43 | b: 2,
44 | },
45 | entropy: 2,
46 | },
47 | ];
48 |
49 | // Act
50 | const combinator = new Combinator();
51 | const result = combinator.getComplexVariant(suspects);
52 |
53 | // Assert
54 | expect(result).toEqual([
55 | {
56 | dependenciesChanges: {
57 | a: 1,
58 | },
59 | },
60 | {
61 | dependenciesChanges: {
62 | a: 1,
63 | b: 2,
64 | },
65 | },
66 | {
67 | dependenciesChanges: {
68 | a: 1,
69 | b: 2,
70 | c: 1,
71 | },
72 | },
73 | ]);
74 | });
75 |
--------------------------------------------------------------------------------
/src/__test__/entropy_service.test.js:
--------------------------------------------------------------------------------
1 | const Sorters = require('../utils/sorters');
2 |
3 | test('byEntropyAndChangesAsc', async () => {
4 | // Arrange
5 | const fakeVariants = [
6 | {
7 | entropy: 3,
8 | dependenciesChanges: {
9 | a: 1,
10 | b: 2,
11 | },
12 | },
13 | {
14 | entropy: 1,
15 | dependenciesChanges: {
16 | a: 1,
17 | b: 2,
18 | },
19 | },
20 | {
21 | entropy: 1,
22 | dependenciesChanges: {
23 | a: 1,
24 | },
25 | },
26 | ];
27 |
28 | // Act
29 | const result = fakeVariants.sort(Sorters.byEntropyAndChangesAsc);
30 |
31 | // Assert
32 | expect(result).toEqual([
33 | {
34 | entropy: 1,
35 | dependenciesChanges: {
36 | a: 1,
37 | },
38 | },
39 | {
40 | entropy: 1,
41 | dependenciesChanges: {
42 | a: 1,
43 | b: 2,
44 | },
45 | },
46 | {
47 | entropy: 3,
48 | dependenciesChanges: {
49 | a: 1,
50 | b: 2,
51 | },
52 | },
53 | ]);
54 | });
55 |
--------------------------------------------------------------------------------
/src/combinator.js:
--------------------------------------------------------------------------------
1 | const { byEntropyAndChangesAsc } = require('./utils/sorters');
2 |
3 | class Combinator {
4 | constructor(logger) {
5 | this._logger = logger;
6 | }
7 |
8 | getSimpleVariant(suspects) {
9 | return suspects.map(dependency => {
10 | const dependenciesChanges = { [dependency.name]: dependency.version };
11 |
12 | return {
13 | dependenciesChanges,
14 | };
15 | });
16 | }
17 |
18 | getComplexVariant(simpleVariantsResults) {
19 | let dependenciesChangesCollector = {};
20 |
21 | const sortedSimpleVariantsResults = [...simpleVariantsResults].sort(
22 | byEntropyAndChangesAsc
23 | );
24 |
25 | const complexVariants = sortedSimpleVariantsResults.map(data => {
26 | const { dependenciesChanges } = data;
27 |
28 | dependenciesChangesCollector = {
29 | ...dependenciesChanges,
30 | ...dependenciesChangesCollector,
31 | };
32 |
33 | return {
34 | dependenciesChanges: dependenciesChangesCollector,
35 | };
36 | });
37 |
38 | return complexVariants;
39 | }
40 | }
41 |
42 | module.exports = Combinator;
43 |
--------------------------------------------------------------------------------
/src/detective.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 |
3 | const removeVersion = fullName =>
4 | fullName
5 | .split('@')
6 | .slice(0, -1)
7 | .join('@');
8 |
9 | const semver = require('semver');
10 |
11 | const byVersionDesc = (a, b) => {
12 | if (semver.lt(a, b)) {
13 | return 1;
14 | }
15 |
16 | if (semver.gt(a, b)) {
17 | return -1;
18 | }
19 |
20 | return 0;
21 | };
22 |
23 | const byVersionAsc = (a, b) => {
24 | if (semver.lt(a, b)) {
25 | return -1;
26 | }
27 |
28 | if (semver.gt(a, b)) {
29 | return 1;
30 | }
31 |
32 | return 0;
33 | };
34 |
35 | const byMarkAndVersionDesc = (a, b) => {
36 | const markDiff = b.mark - a.mark;
37 |
38 | if (markDiff) {
39 | return markDiff;
40 | }
41 |
42 | return byVersionDesc(a.version, b.version);
43 | };
44 |
45 | const byMarkDesc = (a, b) => b.mark - a.mark;
46 |
47 | const byCountInstancesAndVersionDesc = (a, b) => {
48 | const countInstancesDiff = b.countInstances - a.countInstances;
49 |
50 | if (countInstancesDiff) {
51 | return countInstancesDiff;
52 | }
53 |
54 | return byVersionDesc(a.version, b.version);
55 | };
56 |
57 | const markDependency = (versionsPriority, dependencyVersion) => {
58 | if (!versionsPriority) {
59 | return 0;
60 | }
61 |
62 | const position = versionsPriority.indexOf(dependencyVersion);
63 | if (position === -1) {
64 | return 0;
65 | }
66 |
67 | return versionsPriority.length - position;
68 | };
69 |
70 | class Detective {
71 | constructor(packageCollector, logger) {
72 | this._packageCollector = packageCollector;
73 | this._logger = logger;
74 | }
75 |
76 | async _findOptimalVersions(name, effects) {
77 | this._logger.log('start find', name, effects);
78 |
79 | const allVersions = await this._packageCollector.getAllVersions(name);
80 |
81 | const analizedVersion = allVersions.map(version => {
82 | const dependencies = version.dependencies || [];
83 |
84 | return {
85 | ...version,
86 | mark: Object.entries(dependencies)
87 | .reduce((summ, [name, version]) => {
88 | return summ + markDependency(effects[name], version);
89 | }, 0),
90 | };
91 | });
92 |
93 | const bestMatchVersions = analizedVersion
94 | .filter(version => version.mark)
95 | .sort(byMarkAndVersionDesc);
96 |
97 | const bestMatchVersion = bestMatchVersions[0];
98 |
99 | this._logger.log('end find', name, bestMatchVersion);
100 |
101 | return bestMatchVersion;
102 | }
103 |
104 | async getSuspects(root, scoupe) {
105 | const rootPackageName = root.name;
106 | const versions = {};
107 |
108 | Object.entries(scoupe).forEach(([name, data]) => {
109 | // strategy последняя
110 | // const usedVersions = Object.keys(data).sort(byVersionDesc);
111 | // versions[name] = usedVersions;
112 |
113 | // strategy main
114 | const allVersions = Object.values(data);
115 | const popularVersion = _.find(allVersions, ['popular', true]);
116 | versions[name] = [popularVersion.version];
117 | });
118 |
119 | const effects = {};
120 |
121 | Object.values(scoupe).forEach(dependency => {
122 | const allVersions = Object.values(dependency);
123 |
124 | const hasOnlyOneVersion = allVersions.length === 1;
125 | if (hasOnlyOneVersion) {
126 | return; // it is good!
127 | }
128 |
129 | const extraDependencies = allVersions.filter(
130 | dependency => !dependency.popular
131 | );
132 |
133 | extraDependencies.forEach(dependency => {
134 | // TODO usages ???
135 | dependency.usages.forEach(path => {
136 | const carrier = removeVersion(path[1] || path[0]);
137 | const effect = (path[2] && removeVersion(path[2])) || dependency.name;
138 |
139 | if (!effects[carrier]) {
140 | effects[carrier] = [];
141 | }
142 | effects[carrier].push(effect);
143 | });
144 | });
145 | });
146 |
147 | // bad mutation
148 | Object.keys(effects).forEach(carrier => {
149 | effects[carrier] = _.uniq(effects[carrier]);
150 | });
151 |
152 | const suspects = [];
153 |
154 | await Promise.all(
155 | Object.entries(effects).map(async ([name, effects]) => {
156 | if (name === rootPackageName) {
157 | effects.forEach(effect => {
158 | suspects.push({
159 | name: effect,
160 | version: versions[effect][0],
161 | });
162 | });
163 | } else {
164 | const allVersions = await this._packageCollector.getAllVersions(name);
165 |
166 | this._logger.log('find all', name, effects);
167 |
168 | effects.forEach(effect => {
169 | this._logger.log(effect, versions[effect]);
170 | });
171 |
172 | const markedVersions = allVersions.map(version => {
173 | if (!version.dependencies) {
174 | return {
175 | mark: 0,
176 | reason: [],
177 | data: version,
178 | };
179 | }
180 |
181 | const reason = [];
182 | const summary = Object.keys(version.dependencies).length;
183 | const matches = Object.entries(version.dependencies).reduce(
184 | (summ, [name, value]) => {
185 | if (
186 | versions[name] &&
187 | semver.satisfies(versions[name][0], value)
188 | ) {
189 | return summ + 1;
190 | } else {
191 | reason.push(name + '@' + value);
192 | }
193 |
194 | return summ;
195 | },
196 | 0
197 | );
198 |
199 | const mark = summary ? (matches / summary) : 1;
200 |
201 | return {
202 | mark,
203 | reason,
204 | data: version,
205 | };
206 | });
207 |
208 | const topMark = markedVersions.reduce(
209 | (max, item) => Math.max(max, item.mark),
210 | -Infinity
211 | );
212 |
213 | const topMarked = markedVersions.filter(
214 | version => version.mark === topMark
215 | );
216 |
217 | const bestEverVersion = _.find(topMarked, [
218 | 'data.version',
219 | versions[name][0],
220 | ]);
221 |
222 | const rootVersion = root.dependencies[name];
223 | const sourceVersion = _.find(topMarked, [
224 | 'data.version',
225 | rootVersion,
226 | ]);
227 |
228 | const minorChangedVersions = topMarked.filter(data => {
229 | const diff = semver.diff(data.data.version, rootVersion);
230 |
231 | return diff === 'minor' || diff === 'preminor';
232 | });
233 |
234 | const patchChangedVersions = topMarked.filter(data => {
235 | const diff = semver.diff(data.data.version, rootVersion);
236 |
237 | return diff === 'patch' || diff === 'prepatch';
238 | });
239 |
240 | const majorChangedVersions = topMarked.filter(data => {
241 | const diff = semver.diff(data.data.version, rootVersion);
242 |
243 | return diff === 'major' || diff === 'premajor';
244 | });
245 |
246 | const selected =
247 | bestEverVersion ||
248 | sourceVersion ||
249 | minorChangedVersions[0] ||
250 | patchChangedVersions[0] ||
251 | majorChangedVersions[0] ||
252 | topMarked[0];
253 |
254 | this._logger.log('selected', selected);
255 |
256 | suspects.push({
257 | name: selected.data.name,
258 | version: selected.data.version,
259 | });
260 | }
261 | })
262 | );
263 |
264 | return suspects;
265 | }
266 | }
267 |
268 | module.exports = Detective;
269 |
--------------------------------------------------------------------------------
/src/entropy_service.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 |
3 | const chalk = require('chalk');
4 |
5 | const WEIGHTS = {
6 | major: 1,
7 | premajor: 1,
8 | minor: 1,
9 | preminor: 1,
10 | patch: 1,
11 | prepatch: 1,
12 | prerelease: 1,
13 | null: 0,
14 | };
15 |
16 | const semver = require('semver');
17 |
18 | class EntropyService {
19 | constructor(logger) {
20 | this._logger = logger;
21 | }
22 |
23 | scoreEntropy(scoupe) {
24 | let entropy = 0;
25 |
26 | Object.values(scoupe).forEach(dependency => {
27 | const allVersions = Object.values(dependency);
28 |
29 | const mainVersion = _.find(allVersions, ['main', true]);
30 |
31 | allVersions.forEach(data => {
32 | const { countInstances, version } = data;
33 | const cost =
34 | countInstances * WEIGHTS[semver.diff(mainVersion.version, version)];
35 | entropy += cost;
36 | });
37 | });
38 |
39 | return entropy;
40 | }
41 |
42 | getDuplicatesCount(scoupe) {
43 | let duplicatesCount = 0;
44 |
45 | Object.values(scoupe).forEach(dependency => {
46 | const allVersions = Object.values(dependency);
47 |
48 | const duplicatesVersions = allVersions.filter(data => !data.main);
49 |
50 | duplicatesCount += duplicatesVersions.length;
51 | });
52 |
53 | return duplicatesCount;
54 | }
55 |
56 | getInfo(scoupe) {
57 | const entropy = this.scoreEntropy(scoupe);
58 | const duplicatesCount = this.getDuplicatesCount(scoupe);
59 |
60 | if (!duplicatesCount) {
61 | return `not found packages with multiple versions 🎉`;
62 | }
63 |
64 | return `${duplicatesCount} packages with multiple versions, and they spawned ${entropy} duplicates`;
65 | }
66 |
67 | printDuplicates(scoupe) {
68 | const entropy = this.scoreEntropy(scoupe);
69 | const duplicatesCount = this.getDuplicatesCount(scoupe);
70 |
71 | if (duplicatesCount) {
72 | console.log(
73 | ` 🔎 ${duplicatesCount} packages with multiple versions, and they spawned ${entropy} duplicates`
74 | );
75 | } else {
76 | console.log(' 🔎 not found packages with multiple versions 👌');
77 | }
78 |
79 | Object.values(scoupe).forEach(dependency => {
80 | const allVersions = Object.values(dependency);
81 | const hasOneVersion = allVersions.length === 1;
82 |
83 | if (hasOneVersion) {
84 | return;
85 | }
86 |
87 | const mainVersion = _.find(allVersions, ['main', true]);
88 | console.log('\n');
89 | console.log(chalk.bold.yellow(`\ WARNING in ${mainVersion.name}`));
90 | console.log(
91 | ' Multiple version of versions ' +
92 | chalk.bold.green(mainVersion.name) +
93 | ' found:'
94 | );
95 |
96 | allVersions.forEach(data => {
97 | console.log(
98 | ' ' +
99 | chalk.bold.green(data.version + ' ' + (data.main ? '[main]' : ''))
100 | );
101 |
102 | const usages = data.usages.map(path => {
103 | return path.join('/');
104 | });
105 |
106 | const dedupedUsages = data.dedupedUsages.map(path => {
107 | return path.join('/');
108 | });
109 |
110 | usages.forEach(path => {
111 | const isDeduped = !dedupedUsages.includes(path);
112 | let view = ' ';
113 | if (isDeduped) {
114 | view += chalk.grey(path + ' (deduped)');
115 | } else {
116 | view += chalk.bold(path);
117 | }
118 | console.log(view);
119 | });
120 | });
121 | });
122 | }
123 | }
124 |
125 | module.exports = EntropyService;
126 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const buildTreeAsync = require('dependencies-tree-builder');
3 | const EntropyService = require('./entropy_service');
4 | const Combinator = require('./combinator');
5 | const Logger = require('./logger');
6 | const Detective = require('./detective');
7 | const chalk = require('chalk');
8 | const ora = require('ora');
9 | const { byEntropyAndChangesAscWithFault } = require('./utils/sorters');
10 |
11 | const DEFAULT_OPTIONS = {
12 | useCache: true,
13 | viewFullLogs: false,
14 | printSourceTreeDuplicates: false,
15 | printOptimalTreeDuplicates: false,
16 | };
17 |
18 | const defaultLogger = new Logger();
19 |
20 | const spinner = ora({
21 | color: 'yellow',
22 | });
23 |
24 | const main = async (
25 | treeRoot,
26 | options = DEFAULT_OPTIONS,
27 | logger = defaultLogger
28 | ) => {
29 | const start = Date.now();
30 |
31 | logger.setStrategy(options);
32 |
33 | spinner.start('analyzing source dependencies tree');
34 |
35 | const { scoupe, packageCollector } = await buildTreeAsync(
36 | treeRoot,
37 | options,
38 | logger
39 | );
40 |
41 | const entropyService = new EntropyService(logger);
42 |
43 | const sourceEntropy = entropyService.scoreEntropy(scoupe);
44 |
45 | spinner.succeed('source tree: ' + entropyService.getInfo(scoupe));
46 |
47 | if (options.printSourceTreeDuplicates) {
48 | console.log('\n');
49 | console.log(chalk.bold.yellow('SOURCE TREE DESCRIPTION:'));
50 | entropyService.printDuplicates(scoupe);
51 | console.log('\n');
52 | }
53 |
54 | spinner.start('optimizing...');
55 |
56 | const detective = new Detective(packageCollector, logger);
57 | const suspects = await detective.getSuspects(treeRoot, scoupe);
58 |
59 | const applyDependencyChanges = dependenciesChanges => {
60 | const newTreeRoot = _.cloneDeep(treeRoot);
61 |
62 | newTreeRoot.dependencies = {
63 | ...newTreeRoot.dependencies,
64 | ...dependenciesChanges,
65 | };
66 |
67 | return newTreeRoot;
68 | };
69 |
70 | const analizeVariantsAsync = async variants => {
71 | return await Promise.all(
72 | variants.map(async ({ dependenciesChanges }) => {
73 | logger.log(
74 | 'try change root dependency\n' +
75 | JSON.stringify(dependenciesChanges, null, 4)
76 | );
77 |
78 | const variantTreeRoot = applyDependencyChanges(dependenciesChanges);
79 |
80 | const { scoupe } = await buildTreeAsync(
81 | variantTreeRoot,
82 | { ...options, useCache: true },
83 | logger
84 | );
85 |
86 | const entropy = entropyService.scoreEntropy(scoupe);
87 |
88 | logger.log(
89 | 'done analyze change root dependency, entropy: ' +
90 | entropy +
91 | '\n' +
92 | JSON.stringify(dependenciesChanges, null, 4)
93 | );
94 |
95 | return {
96 | dependenciesChanges,
97 | entropy,
98 | };
99 | })
100 | );
101 | };
102 |
103 | const combinator = new Combinator(logger);
104 |
105 | const simpleVariants = combinator.getSimpleVariant(suspects);
106 | const simpleVariantsResults = await analizeVariantsAsync(simpleVariants);
107 |
108 | const complexVariants = combinator.getComplexVariant(simpleVariantsResults);
109 | const complexVariantsResults = await analizeVariantsAsync(complexVariants);
110 |
111 | const allResults = [...simpleVariantsResults, ...complexVariantsResults];
112 |
113 | const optimalVariant = allResults.sort(
114 | byEntropyAndChangesAscWithFault(0.05)
115 | )[0];
116 |
117 | spinner.succeed(`done, ${(Date.now() - start) / 1000}s`);
118 | spinner.start('analyzing optimal dependencies tree');
119 |
120 | const optimalTreeRoot = applyDependencyChanges(
121 | _.get(optimalVariant, 'dependenciesChanges')
122 | );
123 |
124 | const { scoupe: optimalTreeScoupe } = await buildTreeAsync(
125 | optimalTreeRoot,
126 | { ...options, useCache: true },
127 | logger
128 | );
129 |
130 | spinner.succeed('optimal tree: ' + entropyService.getInfo(optimalTreeScoupe));
131 |
132 | if (!_.isEmpty(optimalVariant) && optimalVariant.entropy < sourceEntropy) {
133 | const sourceDependencies = treeRoot.dependencies;
134 | const optimalDependencies = optimalTreeRoot.dependencies;
135 |
136 | console.log(
137 | '\n' + chalk.yellow.bold('suggested update:'.toUpperCase()) + '\n'
138 | );
139 |
140 | let count = 0;
141 | Object.keys(sourceDependencies).map((name, idx) => {
142 | if (
143 | optimalDependencies[name] &&
144 | optimalDependencies[name] !== sourceDependencies[name]
145 | ) {
146 | const view =
147 | _.padEnd(name, 30) +
148 | _.padEnd(sourceDependencies[name], 8) +
149 | ' → ' +
150 | _.padEnd(optimalDependencies[name], 8);
151 |
152 | const styler = count++ % 2 ? chalk.bold : chalk;
153 |
154 | console.log(styler(view));
155 | }
156 | });
157 | console.log('\n');
158 |
159 | if (options.printOptimalTreeDuplicates) {
160 | console.log(chalk.bold.yellow('OPTIMAL TREE DESCRIPTION:'));
161 | entropyService.printDuplicates(optimalTreeScoupe);
162 | console.log('\n');
163 | }
164 |
165 | return optimalTreeRoot;
166 | } else {
167 | console.log('😞 no update recommendation');
168 | }
169 |
170 | return null;
171 | };
172 |
173 | module.exports = async (...args) => {
174 | try {
175 | return await main(...args);
176 | } catch (error) {
177 | spinner.fail('fail');
178 | throw error;
179 | }
180 | }
--------------------------------------------------------------------------------
/src/logger.js:
--------------------------------------------------------------------------------
1 | const fakeLogger = {
2 | log: () => {},
3 | error: () => {},
4 | warn: () => {},
5 | };
6 |
7 | class Logger {
8 | constructor() {
9 | this._defaultStrategy = fakeLogger;
10 | }
11 |
12 | setStrategy(options) {
13 | if (options.viewFullLogs) {
14 | this._strategy = console;
15 | } else {
16 | this._strategy = fakeLogger;
17 | }
18 | }
19 |
20 | getStrategy() {
21 | return this._strategy || this._defaultStrategy;
22 | }
23 |
24 | log(...args) {
25 | return this.getStrategy().log(...args);
26 | }
27 |
28 | error(...args) {
29 | return this.getStrategy().error(...args);
30 | }
31 |
32 | warn(...args) {
33 | return this.getStrategy().warn(...args);
34 | }
35 | }
36 |
37 | module.exports = Logger;
38 |
--------------------------------------------------------------------------------
/src/utils/__test__/sorters.test.js:
--------------------------------------------------------------------------------
1 | const Sorters = require('../sorters');
2 |
3 | test('byEntropyAndChangesAsc', async () => {
4 | // Arrange
5 | const fakeVariants = [
6 | {
7 | entropy: 3,
8 | dependenciesChanges: {
9 | a: 1,
10 | b: 2,
11 | },
12 | },
13 | {
14 | entropy: 1,
15 | dependenciesChanges: {
16 | a: 1,
17 | b: 2,
18 | },
19 | },
20 | {
21 | entropy: 1,
22 | dependenciesChanges: {
23 | a: 1,
24 | },
25 | },
26 | ];
27 |
28 | // Act
29 | const result = fakeVariants.sort(Sorters.byEntropyAndChangesAsc);
30 |
31 | // Assert
32 | expect(result).toEqual([
33 | {
34 | entropy: 1,
35 | dependenciesChanges: {
36 | a: 1,
37 | },
38 | },
39 | {
40 | entropy: 1,
41 | dependenciesChanges: {
42 | a: 1,
43 | b: 2,
44 | },
45 | },
46 | {
47 | entropy: 3,
48 | dependenciesChanges: {
49 | a: 1,
50 | b: 2,
51 | },
52 | },
53 | ]);
54 | });
55 |
--------------------------------------------------------------------------------
/src/utils/sorters.js:
--------------------------------------------------------------------------------
1 | const byEntropyAndChanges = order => (fault = 0) => (a, b) => {
2 | const getEntropy = variant => variant.entropy;
3 | const countChanges = variant =>
4 | Object.keys(variant.dependenciesChanges).length;
5 |
6 | const entropyDiff = order(getEntropy(a), getEntropy(b));
7 | const averageEntropy = (getEntropy(a) + getEntropy(b)) / 2;
8 |
9 | if (entropyDiff && Math.abs(entropyDiff / averageEntropy) > fault) {
10 | return entropyDiff;
11 | }
12 |
13 | const changesDiff = order(countChanges(a), countChanges(b));
14 | return changesDiff;
15 | };
16 |
17 | const asc = (a, b) => a - b;
18 | const desc = (a, b) => b - a;
19 |
20 | module.exports = {
21 | byEntropyAndChangesAsc: byEntropyAndChanges(asc)(0),
22 | byEntropyAndChangesAscWithFault: byEntropyAndChanges(asc),
23 | };
24 |
--------------------------------------------------------------------------------