Skip to content

Commit

Permalink
fund: support multiple funding sources
Browse files Browse the repository at this point in the history
  • Loading branch information
ljharb committed Jan 27, 2020
1 parent ac3739f commit c89c45d
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 125 deletions.
21 changes: 19 additions & 2 deletions docs/content/configuring-npm/package-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ npm also sets a top-level "maintainers" field with your npm user info.
### funding

You can specify an object containing an URL that provides up-to-date
information about ways to help fund development of your package:
information about ways to help fund development of your package, or
a string URL, or an array of these:

"funding": {
"type" : "individual",
Expand All @@ -209,10 +210,26 @@ information about ways to help fund development of your package:
"url" : "https://www.patreon.com/my-account"
}

"funding": "http://example.com/donate"

"funding": [
{
"type" : "individual",
"url" : "http://example.com/donate"
},
"http://example.com/donateAlso",
{
"type" : "patreon",
"url" : "https://www.patreon.com/my-account"
}
]


Users can use the `npm fund` subcommand to list the `funding` URLs of all
dependencies of their project, direct and indirect. A shortcut to visit each
funding url is also available when providing the project name such as:
`npm fund <projectname>`.
`npm fund <projectname>` (when there are multiple URLs, the first one will be
visited)

### files

Expand Down
127 changes: 48 additions & 79 deletions lib/fund.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const readShrinkwrap = require('./install/read-shrinkwrap.js')
const mutateIntoLogicalTree = require('./install/mutate-into-logical-tree.js')
const output = require('./utils/output.js')
const openUrl = require('./utils/open-url.js')
const { getFundingInfo, retrieveFunding, validFundingUrl } = require('./utils/funding.js')
const { getFundingInfo, retrieveFunding, validFundingField, flatCacheSymbol } = require('./utils/funding.js')

const FundConfig = figgyPudding({
browser: {}, // used by ./utils/open-url
Expand Down Expand Up @@ -52,96 +52,56 @@ function printJSON (fundingInfo) {
// level possible, in that process they also carry their dependencies along
// with them, moving those up in the visual tree
function printHuman (fundingInfo, opts) {
// mapping logic that keeps track of seen items in order to be able
// to push all other items from the same type/url in the same place
const seen = new Map()
const flatCache = fundingInfo[flatCacheSymbol];

function seenKey ({ type, url } = {}) {
return url ? String(type) + String(url) : null
}

function setStackedItem (funding, result) {
const key = seenKey(funding)
if (key && !seen.has(key)) seen.set(key, result)
}

function retrieveStackedItem (funding) {
const key = seenKey(funding)
if (key && seen.has(key)) return seen.get(key)
}
const { name, funding, version } = fundingInfo
const printableVersion = version ? `@${version}` : ''
const rootFunding = []//.concat(funding ? retrieveFunding(funding) : []).map(x => x.url)

// ---
const items = Object.keys(flatCache).map((url) => {
const deps = flatCache[url]

const getFundingItems = (fundingItems) =>
Object.keys(fundingItems || {}).map((fundingItemName) => {
// first-level loop, prepare the pretty-printed formatted data
const fundingItem = fundingItems[fundingItemName]
const { version, funding } = fundingItem
const { type, url } = funding || {}
const packages = deps.map((dep) => {
const { name, funding, version } = dep

const printableVersion = version ? `@${version}` : ''
const printableType = type && { label: `type: ${funding.type}` }
const printableUrl = url && { label: `url: ${funding.url}` }
const result = {
fundingItem,
label: fundingItemName + printableVersion,
nodes: []
}

if (printableType) {
result.nodes.push(printableType)
}

if (printableUrl) {
result.nodes.push(printableUrl)
}

setStackedItem(funding, result)

return result
}).reduce((res, result) => {
// recurse and exclude nodes that are going to be stacked together
const { fundingItem } = result
const { dependencies, funding } = fundingItem
const items = getFundingItems(dependencies)
const stackedResult = retrieveStackedItem(funding)
items.forEach(i => result.nodes.push(i))

if (stackedResult && stackedResult !== result) {
stackedResult.label += `, ${result.label}`
items.forEach(i => stackedResult.nodes.push(i))
return res
}

res.push(result)

return res
}, [])

const [ result ] = getFundingItems({
[fundingInfo.name]: {
dependencies: fundingInfo.dependencies,
funding: fundingInfo.funding,
version: fundingInfo.version
return `${name}${printableVersion}`
})

return {
label: url,
nodes: [packages.join(', ')]
}
})

return archy(result, '', { unicode: opts.unicode })

const nodes = [].concat(rootFunding, items)

return archy({ label: `${name}${printableVersion}`, nodes }, '', { unicode: opts.unicode })
}

function openFundingUrl (packageName, cb) {
function openFundingUrl (packageName, fundingSourceNumber, cb) {
function getUrlAndOpen (packageMetadata) {
const { funding } = packageMetadata
const { type, url } = retrieveFunding(funding) || {}
const noFundingError =
new Error(`No funding method available for: ${packageName}`)
noFundingError.code = 'ENOFUND'
const typePrefix = type ? `${type} funding` : 'Funding'
const msg = `${typePrefix} available at the following URL`

if (validFundingUrl(funding)) {
const validSources = [].concat(retrieveFunding(funding)).filter(validFundingField)

if (validSources.length === 1 || (fundingSourceNumber > 0 && fundingSourceNumber <= validSources.length)) {
const { type, url } = validSources[fundingSourceNumber ? fundingSourceNumber - 1 : 0]
const typePrefix = type ? `${type} funding` : 'Funding'
const msg = `${typePrefix} available at the following URL`
openUrl(url, msg, cb)
} else if (packageNumber < 1) {
validSources.forEach(({ type, url }, i) => {
const typePrefix = type ? `${type} funding` : 'Funding'
const msg = `${typePrefix} available at the following URL`
console.log(`${i + 1}: ${msg}: ${url}`)
})
console.log('Run `npm fund ${packageName} 1`, for example, to open the first funding URL')
cb()
} else {
const noFundingError = new Error(`No valid funding method available for: ${packageName}`)
noFundingError.code = 'ENOFUND'

throw noFundingError
}
}
Expand All @@ -161,15 +121,24 @@ function fundCmd (args, cb) {
const opts = FundConfig(npmConfig())
const dir = path.resolve(npm.dir, '..')
const packageName = args[0]
const numberArg = args[1]

const fundingSourceNumber = numberArg && parseInt(numberArg, 10)

if (numberArg && String(fundingSourceNumber) !== numberArg || fundingSourceNumber < 1) {
const err = new Error('`npm fund [<@scope>/]<pkg> <number>` must be given a positive integer')
err.code = 'EFUNDNUMBER'
throw err
}

if (opts.global) {
const err = new Error('`npm fund` does not support globals')
const err = new Error('`npm fund` does not support global packages')
err.code = 'EFUNDGLOBAL'
throw err
}

if (packageName) {
openFundingUrl(packageName, cb)
openFundingUrl(packageName, fundingSourceNumber, cb)
return
}

Expand Down
Loading

0 comments on commit c89c45d

Please sign in to comment.