├── .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 | --------------------------------------------------------------------------------