diff --git a/cmd.js b/cmd.js index 5130255..1b40441 100755 --- a/cmd.js +++ b/cmd.js @@ -7,16 +7,19 @@ const opn = require('opn') const ora = require('ora') const pify = require('pify') const pkgDir = require('pkg-dir') +const pkgUp = require('pkg-up') const readPackageTree = require('read-package-tree') const RegistryClient = require('npm-registry-client') // TODO: use npm-registry-fetch when done const registryUrl = require('registry-url') +const setTimeoutAsync = require('timeout-as-promise') const stripAnsi = require('strip-ansi') const termSize = require('term-size') const textTable = require('text-table') -const setTimeoutAsync = require('timeout-as-promise') +const { readFile } = require('fs') const thanks = require('./') +const readFileAsync = pify(readFile) const readPackageTreeAsync = pify(readPackageTree) const DOWNLOADS_URL = 'https://api.npmjs.org/downloads/point/last-month/' @@ -58,6 +61,9 @@ async function init () { }) const cwd = argv._[0] || process.cwd() + spinner.text = chalk`Reading {cyan direct dependencies} from metadata in {magenta package.json}...` + const directPkgNames = await readDirectPkgNames() + spinner.text = chalk`Reading {cyan dependencies} from package tree in {magenta node_modules}...` const rootPath = await pkgDir(cwd) const packageTree = await readPackageTreeAsync(rootPath) @@ -71,8 +77,8 @@ async function init () { spinner.text = chalk`Fetching package {cyan download counts} from {red npm}...` const downloadCounts = await bulkFetchDownloads(pkgNames) - // Author name -> list of packages (sorted by download count) - const authorsPkgNames = computeAuthorsPkgNames(allPkgs, downloadCounts) + // Author name -> list of packages (sorted by direct dependencies, then download count) + const authorsPkgNames = computeAuthorsPkgNames(allPkgs, downloadCounts, directPkgNames) // Array of author names who are seeking donations const authorsSeeking = Object.keys(authorsPkgNames) @@ -84,15 +90,13 @@ async function init () { if (authorsSeeking.length) { spinner.succeed(chalk`You depend on {cyan ${authorsSeeking.length} authors} who are {magenta seeking donations!} ✨\n`) - printTable(authorsSeeking, authorsPkgNames) + printTable(authorsSeeking, authorsPkgNames, directPkgNames) if (argv.open) openDonateLinks(donateLinks) } else { spinner.info('You don\'t depend on any packages from maintainers seeking donations') } // TODO: compute list of **projects** seeking donations - // TODO: show direct dependencies first in the list - // console.log(readLocalDeps()) } function createRegistryClient () { @@ -128,15 +132,22 @@ async function fetchPkg (client, pkgName) { return client.getAsync(url, opts) } -function printTable (authorsSeeking, authorsPkgNames) { +function printTable (authorsSeeking, authorsPkgNames, directPkgNames) { const rows = authorsSeeking .map(author => { - const authorPkgs = authorsPkgNames[author] + // Highlight direct dependencies in a different color + const authorPkgNames = authorsPkgNames[author] + .map(pkgName => { + return directPkgNames.includes(pkgName) + ? chalk.green.bold(pkgName) + : pkgName + }) + const donateLink = thanks.authors[author].replace(RE_REMOVE_URL_PREFIX, '') return [ - chalk.green(author), + author, chalk.cyan(donateLink), - listWithMaxLen(authorPkgs, termSize().columns - 45) + listWithMaxLen(authorPkgNames, termSize().columns - 45) ] }) @@ -180,26 +191,35 @@ async function bulkFetchDownloads (pkgNames) { return downloads } -function computeAuthorsPkgNames (pkgs, downloadCounts) { +function computeAuthorsPkgNames (pkgs, downloadCounts, directPkgNames) { // author name -> array of package names - const authorPkgs = {} + const authorPkgNames = {} pkgs.forEach(pkg => { pkg.maintainers .map(maintainer => maintainer.name) .forEach(author => { - if (authorPkgs[author] == null) authorPkgs[author] = [] - authorPkgs[author].push(pkg.name) + if (authorPkgNames[author] == null) authorPkgNames[author] = [] + authorPkgNames[author].push(pkg.name) }) }) - // Sort each author's package list by download count - Object.keys(authorPkgs).forEach(author => { - const pkgs = authorPkgs[author] - pkgs.sort((pkg1, pkg2) => downloadCounts[pkg2] - downloadCounts[pkg1]) + // Sort each author's package list by direct dependencies, then download count + // dependencies first in the list + Object.keys(authorPkgNames).forEach(author => { + const authorDirectPkgNames = authorPkgNames[author] + .filter(pkgName => directPkgNames.includes(pkgName)) + + const pkgNames = authorPkgNames[author] + .filter(pkgName => !authorDirectPkgNames.includes(pkgName)) + .sort((pkg1, pkg2) => downloadCounts[pkg2] - downloadCounts[pkg1]) + + pkgNames.unshift(...authorDirectPkgNames) + + authorPkgNames[author] = pkgNames }) - return authorPkgs + return authorPkgNames } function listWithMaxLen (list, maxLen) { @@ -208,7 +228,7 @@ function listWithMaxLen (list, maxLen) { let str = '' for (let i = 0; i < list.length; i++) { const item = (i === 0 ? '' : ', ') + list[i] - if (stripAnsi(str).length + item.length >= maxLen - ELLIPSIS_LENGTH) { + if (stripAnsi(str).length + stripAnsi(item).length >= maxLen - ELLIPSIS_LENGTH) { str += ELLIPSIS.replace('XX', list.length - i) break } @@ -233,3 +253,28 @@ async function openDonateLinks (donateLinks) { spinner.succeed(chalk`Opened {cyan ${len} donate pages} in your {magenta web browser} 💻`) } + +async function readDirectPkgNames () { + const pkgPath = await pkgUp() + const pkgStr = await readFileAsync(pkgPath, 'utf8') + + let pkg + try { + pkg = JSON.parse(pkgStr) + } catch (err) { + err.message = `Failed to parse package.json: ${err.message}` + throw err + } + + return [].concat( + findDeps(pkg, 'dependencies'), + findDeps(pkg, 'devDependencies'), + findDeps(pkg, 'optionalDependencies') + ) + + function findDeps (pkg, type) { + return pkg[type] && typeof pkg[type] === 'object' + ? Object.keys(pkg[type]) + : [] + } +}