diff --git a/cmd.js b/cmd.js index e8ca2d5..680bdfc 100755 --- a/cmd.js +++ b/cmd.js @@ -13,15 +13,15 @@ const registryUrl = require('registry-url') const stripAnsi = require('strip-ansi') const termSize = require('term-size') const textTable = require('text-table') -const { promisify } = require('util') +const setTimeoutAsync = require('timeout-as-promise') const thanks = require('./') const readPackageTreeAsync = pify(readPackageTree) -const setTimeoutAsync = promisify(setTimeout) const DOWNLOADS_URL = 'https://api.npmjs.org/downloads/point/last-month/' const DOWNLOADS_URL_LIMIT = 128 +const RE_REMOVE_URL_PREFIX = /https?:\/\/(www\.)?/ const spinner = ora({ spinner: 'moon', @@ -49,7 +49,6 @@ async function init () { }) const cwd = argv._[0] || process.cwd() - // Get all packages in the nearest `node_modules` folder spinner.text = chalk`Reading {cyan dependencies} from package tree in {magenta node_modules}...` const rootPath = await pkgDir(cwd) const packageTree = await readPackageTreeAsync(rootPath) @@ -58,83 +57,33 @@ async function init () { // not include the list of maintainers spinner.text = chalk`Fetching package {cyan maintainers} from {red npm}...` const pkgNames = packageTree.children.map(node => node.package.name) - const allPkgs = await Promise.all(pkgNames.map(fetchPkg)) + const allPkgs = await Promise.all(pkgNames.map(pkgName => fetchPkg(client, pkgName))) - // Fetch download counts for each package spinner.text = chalk`Fetching package {cyan download counts} from {red npm}...` const downloadCounts = await bulkFetchDownloads(pkgNames) - // Author name -> list of packages, ordered by download count - const authorInfos = computeAuthorInfos(allPkgs, downloadCounts) + // Author name -> list of packages (sorted by download count) + const authorsPkgNames = computeAuthorsPkgNames(allPkgs, downloadCounts) - // TODO: compute list of **projects** seeking donations - // TODO: show direct dependencies first in the list - - const donateLinks = [] - - const rows = Object.keys(authorInfos) + // Array of author names who are seeking donations + const authorsSeeking = Object.keys(authorsPkgNames) .filter(author => thanks.authors[author] != null) - .sort((author1, author2) => authorInfos[author2].length - authorInfos[author1].length) - .map(author => { - const authorPkgs = authorInfos[author] - const donateLink = thanks.authors[author] - donateLinks.push(donateLink) - const prettyDonateLink = donateLink.replace(/https?:\/\/(www\.)?/, '') - return [ - chalk.green(author), - chalk.cyan(prettyDonateLink), - listWithMaxLen(authorPkgs, termSize().columns - 45) - ] - }) + .sort((author1, author2) => authorsPkgNames[author2].length - authorsPkgNames[author1].length) - rows.unshift([ - chalk.underline('Author'), - chalk.underline('Where to Donate'), - chalk.underline('Dependencies') - ]) + const donateLinks = authorsSeeking + .map(author => thanks.authors[author]) - if (rows.length) { - spinner.succeed(chalk`You depend on {cyan ${rows.length} authors} who are {magenta seeking donations!} ✨\n`) - printTable(rows) - if (argv.open) openDonateLinks() + if (authorsSeeking.length) { + spinner.succeed(chalk`You depend on {cyan ${authorsSeeking.length} authors} who are {magenta seeking donations!} ✨\n`) + printTable(authorsSeeking, authorsPkgNames) + if (argv.open) openDonateLinks(donateLinks) } else { - spinner.succeed('You don\'t depend on any packages from maintainers seeking donations') - } - - async function openDonateLinks () { - const spinner = ora({ - spinner: 'hearts', - text: chalk`Opening {cyan donate pages} in your {magenta web browser}...` - }).start() - - await setTimeoutAsync(1000) - - for (let donateLink of donateLinks) { - await opn(donateLink, { wait: false }) - } - spinner.succeed() - } - - function printTable (rows) { - const tableOpts = { - stringLength: str => stripAnsi(str).length - } - const table = textTable(rows, tableOpts) - console.log(table + '\n') + spinner.info('You don\'t depend on any packages from maintainers seeking donations') } - async function fetchPkg (pkgName) { - // Note: The registry does not support fetching versions for scoped packages - const url = isScopedPkg(pkgName) - ? `${registryUrl()}${pkgName.replace('/', '%2F')}` - : `${registryUrl()}${pkgName}/latest` - - const opts = { - timeout: 30 * 1000, - staleOk: true - } - return client.getAsync(url, opts) - } + // TODO: compute list of **projects** seeking donations + // TODO: show direct dependencies first in the list + // console.log(readLocalDeps()) } function createRegistryClient () { @@ -157,6 +106,44 @@ function isScopedPkg (pkgName) { return pkgName.includes('/') } +async function fetchPkg (client, pkgName) { + // Note: The registry does not support fetching versions for scoped packages + const url = isScopedPkg(pkgName) + ? `${registryUrl()}${pkgName.replace('/', '%2F')}` + : `${registryUrl()}${pkgName}/latest` + + const opts = { + timeout: 30 * 1000, + staleOk: true + } + return client.getAsync(url, opts) +} + +function printTable (authorsSeeking, authorsPkgNames) { + const rows = authorsSeeking + .map(author => { + const authorPkgs = authorsPkgNames[author] + const donateLink = thanks.authors[author].replace(RE_REMOVE_URL_PREFIX, '') + return [ + chalk.green(author), + chalk.cyan(donateLink), + listWithMaxLen(authorPkgs, termSize().columns - 45) + ] + }) + + rows.unshift([ + chalk.underline('Author'), + chalk.underline('Where to Donate'), + chalk.underline('Dependencies') + ]) + + const opts = { + stringLength: str => stripAnsi(str).length + } + const table = textTable(rows, opts) + console.log(table + '\n') +} + async function bulkFetchDownloads (pkgNames) { // A few notes: // - bulk queries do not support scoped packages @@ -184,26 +171,26 @@ async function bulkFetchDownloads (pkgNames) { return downloads } -function computeAuthorInfos (pkgs, downloadCounts) { +function computeAuthorsPkgNames (pkgs, downloadCounts) { // author name -> array of package names - const authorInfos = {} + const authorPkgs = {} pkgs.forEach(pkg => { pkg.maintainers .map(maintainer => maintainer.name) .forEach(author => { - if (authorInfos[author] == null) authorInfos[author] = [] - authorInfos[author].push(pkg.name) + if (authorPkgs[author] == null) authorPkgs[author] = [] + authorPkgs[author].push(pkg.name) }) }) // Sort each author's package list by download count - Object.keys(authorInfos).forEach(author => { - const pkgs = authorInfos[author] + Object.keys(authorPkgs).forEach(author => { + const pkgs = authorPkgs[author] pkgs.sort((pkg1, pkg2) => downloadCounts[pkg2] - downloadCounts[pkg1]) }) - return authorInfos + return authorPkgs } function listWithMaxLen (list, maxLen) { @@ -220,3 +207,19 @@ function listWithMaxLen (list, maxLen) { } return str } + +async function openDonateLinks (donateLinks) { + console.log(donateLinks) + const len = donateLinks.length + + const spinner = ora({ + text: chalk`Opening {cyan ${len} donate pages} in your {magenta web browser}...` + }).start() + + for (let donateLink of donateLinks) { + await opn(donateLink, { wait: false }) + await setTimeoutAsync(2000) + } + + spinner.succeed(chalk`Opened {cyan ${len} donate pages} in your {magenta web browser}`) +} diff --git a/package.json b/package.json index 85dc46a..111e9ec 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,13 @@ "ora": "^1.4.0", "pify": "^3.0.0", "pkg-dir": "^2.0.0", + "pkg-up": "^2.0.0", "read-package-tree": "^5.1.6", "registry-url": "^3.1.0", "strip-ansi": "^4.0.0", "term-size": "^1.2.0", - "text-table": "^0.2.0" + "text-table": "^0.2.0", + "timeout-as-promise": "^1.0.0" }, "devDependencies": { "standard": "*"