npm-pkg-stats
Advanced tools
Comparing version 1.4.0 to 1.5.0
205
api.js
@@ -1,18 +0,18 @@ | ||
const dayjs = require('dayjs'); | ||
const getRepoUrl = require('get-repository-url'); | ||
const parseRepoUrl = require('parse-github-url'); | ||
const parseGithubUrl = require('parse-github-url'); | ||
const R = require('ramda'); | ||
const Table = require('cli-table'); | ||
const { GraphQLClient } = require('graphql-request'); | ||
const { | ||
calcRatio, | ||
formatNumber, | ||
formatPercentage, | ||
formatSize, | ||
} = require('./util'); | ||
const util = require('./util'); | ||
const nilValue = '--'; | ||
const api = {}; | ||
const makeQuery = (package, owner) => `{ | ||
repository(name: "${package}", owner: "${owner}"){ | ||
api.nilValue = '--'; | ||
api.getRepoUrl = async pkg => await getRepoUrl(pkg); | ||
api.makeOwnerAndPkgNameBy = R.pipe(parseGithubUrl, R.pick(['owner', 'name'])); | ||
api.makeQuery = (pkg, owner) => `{ | ||
repository(name: "${pkg}", owner: "${owner}"){ | ||
openIssues: issues(filterBy: { states: [OPEN]}) { | ||
@@ -52,20 +52,14 @@ totalCount | ||
function makeVerticalTable ({ | ||
githubStats, | ||
npmStats, | ||
package, | ||
}) { | ||
const stats = { | ||
...npmStats, | ||
...githubStats, | ||
}; | ||
const tableStyle = { | ||
head: ['magenta'], | ||
}; | ||
api.makeVerticalTable = function makeVerticalTable({ pkg, ...stats }) { | ||
const table = new Table({ | ||
style: { | ||
head: ['magenta'], | ||
}, | ||
head: ['package', package], | ||
style: tableStyle, | ||
head: ['package', pkg], | ||
}); | ||
const rows = R.pipe( | ||
R.keys, | ||
R.map(key => ({ [key]: stats[key] })) | ||
R.map(key => ({ [key]: stats[key] })), | ||
)(stats); | ||
@@ -75,18 +69,29 @@ table.push(...rows); | ||
return table; | ||
} | ||
}; | ||
function makeNpmStats ({ bundlephobiaData, npmDownloadData }) { | ||
const weeklyNpmDownloads = R.pipe( | ||
R.prop('downloads'), | ||
formatNumber | ||
)(npmDownloadData); | ||
api.makeHorizontalTable = function makeHorizontalTable({ | ||
labelList, | ||
valueList, | ||
}) { | ||
const table = new Table({ | ||
style: tableStyle, | ||
head: labelList, | ||
}); | ||
table.push(...valueList); | ||
return table; | ||
}; | ||
api.makeNpmStats = function makeNpmStats({ | ||
bundlephobiaData, | ||
npmDownloadData, | ||
}) { | ||
return { | ||
version: R.pipe( | ||
R.prop('version'), | ||
R.defaultTo(nilValue) | ||
R.defaultTo(api.nilValue), | ||
)(bundlephobiaData), | ||
dependencies: R.pipe( | ||
R.prop('dependencyCount'), | ||
formatNumber | ||
util.formatNumber, | ||
)(bundlephobiaData), | ||
@@ -97,18 +102,30 @@ 'gzip size': R.pipe( | ||
R.isNil, | ||
R.always(nilValue), | ||
R.always(api.nilValue), | ||
R.pipe( | ||
formatSize, | ||
({ size, unit }) => `${parseFloat(size).toFixed(1)} ${unit}` | ||
) | ||
) | ||
util.formatSize, | ||
({ size, unit }) => `${parseFloat(size).toFixed(1)} ${unit}`, | ||
), | ||
), | ||
)(bundlephobiaData), | ||
'weekly npm downloads': weeklyNpmDownloads, | ||
'weekly npm downloads': R.pipe( | ||
R.prop('downloads'), | ||
util.formatNumber, | ||
)(npmDownloadData), | ||
}; | ||
} | ||
}; | ||
function makeGithubStats ({ githubData }) { | ||
const openIssues = R.path(['repository', 'openIssues', 'totalCount'], githubData); | ||
const closedIssues = R.path(['repository', 'closedIssues', 'totalCount'], githubData); | ||
api.makeGithubStats = function makeGithubStats({ githubData = {} }) { | ||
const openIssues = R.path( | ||
['repository', 'openIssues', 'totalCount'], | ||
githubData, | ||
); | ||
const closedIssues = R.path( | ||
['repository', 'closedIssues', 'totalCount'], | ||
githubData, | ||
); | ||
const openPRs = R.path(['repository', 'openPRs', 'totalCount'], githubData); | ||
const closedPRs = R.path(['repository', 'closedPRs', 'totalCount'], githubData); | ||
const closedPRs = R.path( | ||
['repository', 'closedPRs', 'totalCount'], | ||
githubData, | ||
); | ||
@@ -118,60 +135,64 @@ return { | ||
R.path(['repository', 'stargazers', 'totalCount']), | ||
formatNumber | ||
util.formatNumber, | ||
)(githubData), | ||
'open PRs': formatNumber(openPRs), | ||
'open PRs (% of total)': formatPercentage(calcRatio(openPRs, closedPRs)), | ||
'closed PRs': formatNumber(closedPRs), | ||
'open issues': formatNumber(openIssues), | ||
'open issues (% of total)': formatPercentage(calcRatio(openIssues, closedIssues)), | ||
'closed issues': formatNumber(closedIssues), | ||
'open PRs': util.formatNumber(openPRs), | ||
'open PRs (% of total)': util.formatPercentage( | ||
util.calcRatio(openPRs, closedPRs), | ||
), | ||
'closed PRs': util.formatNumber(closedPRs), | ||
'open issues': util.formatNumber(openIssues), | ||
'open issues (% of total)': util.formatPercentage( | ||
util.calcRatio(openIssues, closedIssues), | ||
), | ||
'closed issues': util.formatNumber(closedIssues), | ||
'last release': R.pipe( | ||
R.path(['repository', 'releases', 'nodes']), | ||
R.defaultTo([]), | ||
R.head, | ||
R.prop('publishedAt'), | ||
date => dayjs(date).format('YYYY-MM-DD') | ||
util.formatDate, | ||
)(githubData), | ||
license: R.pipe( | ||
R.path(['repository', 'licenseInfo', 'name']), | ||
R.defaultTo(nilValue) | ||
R.defaultTo(api.nilValue), | ||
)(githubData), | ||
}; | ||
} | ||
}; | ||
async function getStats (package, token) { | ||
api.getStats = R.curry(async function getStats(token, pkg) { | ||
if (R.isNil(token)) { | ||
console.error('No NPM_PKG_STATS_TOKEN found in your environment variables. Please follow the installation instructions.'); | ||
return; | ||
throw new Error( | ||
'No NPM_PKG_STATS_TOKEN found in your environment variables. Please follow the installation instructions: https://github.com/jacobworrel/npm-pkg-stats#installation--usage.', | ||
); | ||
} | ||
const [ bundlephobiaData, npmDownloadData ] = await Promise.all([ | ||
fetchBundlephobiaData(package), | ||
fetchNpmDownload(package), | ||
const [bundlephobiaData, npmDownloadData] = await Promise.all([ | ||
api.fetchBundlephobiaData(pkg), | ||
api.fetchNpmDownload(pkg), | ||
]); | ||
const npmStats = api.makeNpmStats({ bundlephobiaData, npmDownloadData }); | ||
const npmStats = makeNpmStats({ bundlephobiaData, npmDownloadData }); | ||
const repoUrl = await getRepoUrl(package); | ||
const repoUrl = await api.getRepoUrl(pkg); | ||
if (R.isNil(repoUrl)) { | ||
console.warn(`Requested package has no repository url in package.json so we were unable to gather stats from GitHub.`); | ||
console.log(makeVerticalTable({ npmStats, package }).toString()); | ||
return; | ||
console.warn( | ||
`Requested package "${pkg}" has no repository url in package.json so we were unable to gather stats from GitHub.`, | ||
); | ||
return { | ||
pkg, | ||
...npmStats, | ||
}; | ||
} | ||
const { | ||
owner, | ||
name: githubPackageName, | ||
} = R.pipe( | ||
parseRepoUrl, | ||
R.pick(['owner', 'name']) | ||
)(repoUrl); | ||
const { owner, name: githubPackageName } = api.makeOwnerAndPkgNameBy(repoUrl); | ||
const githubData = await api.fetchGithubData(githubPackageName, owner, token); | ||
const githubStats = api.makeGithubStats({ githubData }); | ||
const githubData = await fetchGithubData(githubPackageName, owner, token); | ||
return { | ||
pkg, | ||
...npmStats, | ||
...githubStats, | ||
}; | ||
}); | ||
const githubStats = makeGithubStats({ githubData }); | ||
console.log('\n'); | ||
console.log(makeVerticalTable({ npmStats, githubStats, package }).toString()); | ||
} | ||
async function fetchGithubData (package, owner, token) { | ||
api.fetchGithubData = async function fetchGithubData(pkg, owner, token) { | ||
const graphQLClient = new GraphQLClient('https://api.github.com/graphql', { | ||
@@ -182,17 +203,17 @@ headers: { | ||
}); | ||
return await graphQLClient.request(makeQuery(package, owner)); | ||
} | ||
return await graphQLClient.request(api.makeQuery(pkg, owner)); | ||
}; | ||
async function fetchBundlephobiaData (package) { | ||
const resp = await fetch(`https://bundlephobia.com/api/size?package=${package}`); | ||
api.fetchBundlephobiaData = async function fetchBundlephobiaData(pkg) { | ||
const resp = await fetch(`https://bundlephobia.com/api/size?package=${pkg}`); | ||
return await resp.json(); | ||
} | ||
}; | ||
async function fetchNpmDownload (package) { | ||
const resp = await fetch(`https://api.npmjs.org/downloads/point/last-week/${package}`); | ||
api.fetchNpmDownload = async function fetchNpmDownload(pkg) { | ||
const resp = await fetch( | ||
`https://api.npmjs.org/downloads/point/last-week/${pkg}`, | ||
); | ||
return await resp.json(); | ||
} | ||
}; | ||
module.exports = { | ||
getStats, | ||
}; | ||
module.exports = api; |
40
index.js
#!/usr/bin/env node | ||
const api = require('./api'); | ||
const chalk = require('chalk'); | ||
@@ -7,12 +8,11 @@ const figlet = require('figlet'); | ||
const R = require('ramda'); | ||
const util = require('./util'); | ||
const { getStats } = require('./api'); | ||
/** | ||
* TODO | ||
* - add support for multiple packages (ie. comparison mode) | ||
*/ | ||
const [ package ] = R.drop(2, process.argv); | ||
const pkgList = R.drop(2, process.argv); | ||
const token = process.env.NPM_PKG_STATS_TOKEN; | ||
@@ -29,4 +29,32 @@ | ||
getStats(package, token) | ||
.then(() => spinner.stop()) | ||
.catch(err => console.error(err)); | ||
Promise.all(R.map(getStats(token), pkgList)) | ||
.then( | ||
R.pipe( | ||
R.tap(() => console.log('\n')), | ||
R.ifElse( | ||
R.pipe(R.length, R.equals(1)), | ||
R.pipe(R.head, api.makeVerticalTable, R.toString, console.log), | ||
pkgStatsList => { | ||
const labelList = R.pipe( | ||
R.reduce( | ||
(result, pkgStats) => R.concat(R.keys(pkgStats), result), | ||
[], | ||
), | ||
R.uniq, | ||
)(pkgStatsList); | ||
const valueList = R.map( | ||
R.pipe(R.values, util.fillWith(api.nilValue, labelList.length)), | ||
pkgStatsList, | ||
); | ||
console.log( | ||
api.makeHorizontalTable({ labelList, valueList }).toString(), | ||
); | ||
}, | ||
), | ||
() => spinner.stop(), | ||
), | ||
) | ||
.catch(err => { | ||
console.error(err); | ||
spinner.stop(); | ||
}); |
{ | ||
"name": "npm-pkg-stats", | ||
"bin": "./index.js", | ||
"version": "1.4.0", | ||
"version": "1.5.0", | ||
"description": "A CLI tool that gets stats on NPM packages.", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"lint": "eslint . --ext .js", | ||
"test": "jest", | ||
"test:watch": "jest --watchAll", | ||
"test:cover": "jest --coverage", | ||
"start": "node index.js" | ||
@@ -27,3 +30,14 @@ }, | ||
"ramda": "^0.26.1" | ||
}, | ||
"devDependencies": { | ||
"eslint": "^6.6.0", | ||
"eslint-config-prettier": "^6.5.0", | ||
"eslint-plugin-prettier": "^3.1.1", | ||
"jest": "^24.9.0", | ||
"prettier": "1.19.1" | ||
}, | ||
"prettier": { | ||
"trailingComma": "all", | ||
"singleQuote": true | ||
} | ||
} |
@@ -60,2 +60,20 @@ # npm-pkg-stats | ||
### Multiple Packages | ||
It also works if you want to compare multiple packages side by side. | ||
For example, `npm-pkg-stats ramda lodash underscore` prints the following to the console: | ||
``` | ||
┌────────────┬─────────┬──────────────┬───────────┬──────────────────────┬──────────────┬──────────┬───────────────────────┬────────────┬─────────────┬──────────────────────────┬───────────────┬──────────────┬─────────────┐ | ||
│ pkg │ version │ dependencies │ gzip size │ weekly npm downloads │ github stars │ open PRs │ open PRs (% of total) │ closed PRs │ open issues │ open issues (% of total) │ closed issues │ last release │ license │ | ||
├────────────┼─────────┼──────────────┼───────────┼──────────────────────┼──────────────┼──────────┼───────────────────────┼────────────┼─────────────┼──────────────────────────┼───────────────┼──────────────┼─────────────┤ | ||
│ ramda │ 0.26.1 │ 0 │ 12.3 kB │ 5,611,712 │ 17,525 │ 97 │ 24.31% │ 302 │ 172 │ 12.49% │ 1,205 │ 2019-05-26 │ MIT License │ | ||
├────────────┼─────────┼──────────────┼───────────┼──────────────────────┼──────────────┼──────────┼───────────────────────┼────────────┼─────────────┼──────────────────────────┼───────────────┼──────────────┼─────────────┤ | ||
│ lodash │ 4.17.15 │ 0 │ 24.3 kB │ 25,955,925 │ 42,361 │ 11 │ 2.39% │ 449 │ 8 │ 0.23% │ 3,516 │ 2016-01-12 │ Other │ | ||
├────────────┼─────────┼──────────────┼───────────┼──────────────────────┼──────────────┼──────────┼───────────────────────┼────────────┼─────────────┼──────────────────────────┼───────────────┼──────────────┼─────────────┤ | ||
│ underscore │ 1.9.1 │ 0 │ 6.3 kB │ 6,645,455 │ 24,984 │ 53 │ 6.83% │ 723 │ 70 │ 5.22% │ 1,272 │ 2019-11-17 │ MIT License │ | ||
└────────────┴─────────┴──────────────┴───────────┴──────────────────────┴──────────────┴──────────┴───────────────────────┴────────────┴─────────────┴──────────────────────────┴───────────────┴──────────────┴─────────────┘ | ||
``` | ||
## Stats | ||
@@ -62,0 +80,0 @@ |
31
util.js
@@ -0,5 +1,6 @@ | ||
const dayjs = require('dayjs'); | ||
const numeral = require('numeral'); | ||
const R = require('ramda'); | ||
function calcRatio (open, closed) { | ||
function calcRatio(open, closed) { | ||
const total = R.add(open, closed); | ||
@@ -9,7 +10,11 @@ return open / total; | ||
function formatNumber (x) { | ||
function formatDate(date) { | ||
return dayjs(date).format('YYYY-MM-DD'); | ||
} | ||
function formatNumber(x) { | ||
return numeral(x).format('0,0'); | ||
} | ||
function formatPercentage (x) { | ||
function formatPercentage(x) { | ||
return numeral(x).format('0.00%'); | ||
@@ -19,23 +24,29 @@ } | ||
// copied from bundlephobia source | ||
function formatSize (value) { | ||
function formatSize(value) { | ||
let unit, size; | ||
if (Math.log10(value) < 3) { | ||
unit = 'B'; | ||
size = value | ||
size = value; | ||
} else if (Math.log10(value) < 6) { | ||
unit = 'kB'; | ||
size = value / 1024 | ||
size = value / 1024; | ||
} else { | ||
unit = 'mB'; | ||
size = value / 1024 / 1024 | ||
size = value / 1024 / 1024; | ||
} | ||
return { unit, size } | ||
return { unit, size }; | ||
} | ||
const fillWith = R.curry((defaultValue, length, list) => | ||
R.times(idx => R.defaultTo(defaultValue, list[idx]), length), | ||
); | ||
module.exports = { | ||
calcRatio, | ||
fillWith, | ||
formatDate, | ||
formatNumber, | ||
formatPercentage, | ||
calcRatio, | ||
formatSize, | ||
}; | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
24023
9
486
1
92
5
0