|
@ -13,15 +13,15 @@ const registryUrl = require('registry-url') |
|
|
const stripAnsi = require('strip-ansi') |
|
|
const stripAnsi = require('strip-ansi') |
|
|
const termSize = require('term-size') |
|
|
const termSize = require('term-size') |
|
|
const textTable = require('text-table') |
|
|
const textTable = require('text-table') |
|
|
const { promisify } = require('util') |
|
|
const setTimeoutAsync = require('timeout-as-promise') |
|
|
|
|
|
|
|
|
const thanks = require('./') |
|
|
const thanks = require('./') |
|
|
|
|
|
|
|
|
const readPackageTreeAsync = pify(readPackageTree) |
|
|
const readPackageTreeAsync = pify(readPackageTree) |
|
|
const setTimeoutAsync = promisify(setTimeout) |
|
|
|
|
|
|
|
|
|
|
|
const DOWNLOADS_URL = 'https://api.npmjs.org/downloads/point/last-month/' |
|
|
const DOWNLOADS_URL = 'https://api.npmjs.org/downloads/point/last-month/' |
|
|
const DOWNLOADS_URL_LIMIT = 128 |
|
|
const DOWNLOADS_URL_LIMIT = 128 |
|
|
|
|
|
const RE_REMOVE_URL_PREFIX = /https?:\/\/(www\.)?/ |
|
|
|
|
|
|
|
|
const spinner = ora({ |
|
|
const spinner = ora({ |
|
|
spinner: 'moon', |
|
|
spinner: 'moon', |
|
@ -49,7 +49,6 @@ async function init () { |
|
|
}) |
|
|
}) |
|
|
const cwd = argv._[0] || process.cwd() |
|
|
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}...` |
|
|
spinner.text = chalk`Reading {cyan dependencies} from package tree in {magenta node_modules}...` |
|
|
const rootPath = await pkgDir(cwd) |
|
|
const rootPath = await pkgDir(cwd) |
|
|
const packageTree = await readPackageTreeAsync(rootPath) |
|
|
const packageTree = await readPackageTreeAsync(rootPath) |
|
@ -58,83 +57,33 @@ async function init () { |
|
|
// not include the list of maintainers
|
|
|
// not include the list of maintainers
|
|
|
spinner.text = chalk`Fetching package {cyan maintainers} from {red npm}...` |
|
|
spinner.text = chalk`Fetching package {cyan maintainers} from {red npm}...` |
|
|
const pkgNames = packageTree.children.map(node => node.package.name) |
|
|
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}...` |
|
|
spinner.text = chalk`Fetching package {cyan download counts} from {red npm}...` |
|
|
const downloadCounts = await bulkFetchDownloads(pkgNames) |
|
|
const downloadCounts = await bulkFetchDownloads(pkgNames) |
|
|
|
|
|
|
|
|
// Author name -> list of packages, ordered by download count
|
|
|
// Author name -> list of packages (sorted by download count)
|
|
|
const authorInfos = computeAuthorInfos(allPkgs, downloadCounts) |
|
|
const authorsPkgNames = computeAuthorsPkgNames(allPkgs, downloadCounts) |
|
|
|
|
|
|
|
|
// TODO: compute list of **projects** seeking donations
|
|
|
// Array of author names who are seeking donations
|
|
|
// TODO: show direct dependencies first in the list
|
|
|
const authorsSeeking = Object.keys(authorsPkgNames) |
|
|
|
|
|
|
|
|
const donateLinks = [] |
|
|
|
|
|
|
|
|
|
|
|
const rows = Object.keys(authorInfos) |
|
|
|
|
|
.filter(author => thanks.authors[author] != null) |
|
|
.filter(author => thanks.authors[author] != null) |
|
|
.sort((author1, author2) => authorInfos[author2].length - authorInfos[author1].length) |
|
|
.sort((author1, author2) => authorsPkgNames[author2].length - authorsPkgNames[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) |
|
|
|
|
|
] |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
rows.unshift([ |
|
|
const donateLinks = authorsSeeking |
|
|
chalk.underline('Author'), |
|
|
.map(author => thanks.authors[author]) |
|
|
chalk.underline('Where to Donate'), |
|
|
|
|
|
chalk.underline('Dependencies') |
|
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
|
|
if (rows.length) { |
|
|
if (authorsSeeking.length) { |
|
|
spinner.succeed(chalk`You depend on {cyan ${rows.length} authors} who are {magenta seeking donations!} ✨\n`) |
|
|
spinner.succeed(chalk`You depend on {cyan ${authorsSeeking.length} authors} who are {magenta seeking donations!} ✨\n`) |
|
|
printTable(rows) |
|
|
printTable(authorsSeeking, authorsPkgNames) |
|
|
if (argv.open) openDonateLinks() |
|
|
if (argv.open) openDonateLinks(donateLinks) |
|
|
} else { |
|
|
} else { |
|
|
spinner.succeed('You don\'t depend on any packages from maintainers seeking donations') |
|
|
spinner.info('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') |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function fetchPkg (pkgName) { |
|
|
// TODO: compute list of **projects** seeking donations
|
|
|
// Note: The registry does not support fetching versions for scoped packages
|
|
|
// TODO: show direct dependencies first in the list
|
|
|
const url = isScopedPkg(pkgName) |
|
|
// console.log(readLocalDeps())
|
|
|
? `${registryUrl()}${pkgName.replace('/', '%2F')}` |
|
|
|
|
|
: `${registryUrl()}${pkgName}/latest` |
|
|
|
|
|
|
|
|
|
|
|
const opts = { |
|
|
|
|
|
timeout: 30 * 1000, |
|
|
|
|
|
staleOk: true |
|
|
|
|
|
} |
|
|
|
|
|
return client.getAsync(url, opts) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function createRegistryClient () { |
|
|
function createRegistryClient () { |
|
@ -157,6 +106,44 @@ function isScopedPkg (pkgName) { |
|
|
return pkgName.includes('/') |
|
|
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) { |
|
|
async function bulkFetchDownloads (pkgNames) { |
|
|
// A few notes:
|
|
|
// A few notes:
|
|
|
// - bulk queries do not support scoped packages
|
|
|
// - bulk queries do not support scoped packages
|
|
@ -184,26 +171,26 @@ async function bulkFetchDownloads (pkgNames) { |
|
|
return downloads |
|
|
return downloads |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function computeAuthorInfos (pkgs, downloadCounts) { |
|
|
function computeAuthorsPkgNames (pkgs, downloadCounts) { |
|
|
// author name -> array of package names
|
|
|
// author name -> array of package names
|
|
|
const authorInfos = {} |
|
|
const authorPkgs = {} |
|
|
|
|
|
|
|
|
pkgs.forEach(pkg => { |
|
|
pkgs.forEach(pkg => { |
|
|
pkg.maintainers |
|
|
pkg.maintainers |
|
|
.map(maintainer => maintainer.name) |
|
|
.map(maintainer => maintainer.name) |
|
|
.forEach(author => { |
|
|
.forEach(author => { |
|
|
if (authorInfos[author] == null) authorInfos[author] = [] |
|
|
if (authorPkgs[author] == null) authorPkgs[author] = [] |
|
|
authorInfos[author].push(pkg.name) |
|
|
authorPkgs[author].push(pkg.name) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
// Sort each author's package list by download count
|
|
|
// Sort each author's package list by download count
|
|
|
Object.keys(authorInfos).forEach(author => { |
|
|
Object.keys(authorPkgs).forEach(author => { |
|
|
const pkgs = authorInfos[author] |
|
|
const pkgs = authorPkgs[author] |
|
|
pkgs.sort((pkg1, pkg2) => downloadCounts[pkg2] - downloadCounts[pkg1]) |
|
|
pkgs.sort((pkg1, pkg2) => downloadCounts[pkg2] - downloadCounts[pkg1]) |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
return authorInfos |
|
|
return authorPkgs |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function listWithMaxLen (list, maxLen) { |
|
|
function listWithMaxLen (list, maxLen) { |
|
@ -220,3 +207,19 @@ function listWithMaxLen (list, maxLen) { |
|
|
} |
|
|
} |
|
|
return str |
|
|
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}`) |
|
|
|
|
|
} |
|
|