@@ -14,13 +14,14 @@ const readShrinkwrap = require('./install/read-shrinkwrap.js')
14
14
const mutateIntoLogicalTree = require ( './install/mutate-into-logical-tree.js' )
15
15
const output = require ( './utils/output.js' )
16
16
const openUrl = require ( './utils/open-url.js' )
17
- const { getFundingInfo, retrieveFunding, validFundingUrl } = require ( './utils/funding.js' )
17
+ const { getFundingInfo, retrieveFunding, validFundingField , flatCacheSymbol } = require ( './utils/funding.js' )
18
18
19
19
const FundConfig = figgyPudding ( {
20
20
browser : { } , // used by ./utils/open-url
21
21
global : { } ,
22
22
json : { } ,
23
- unicode : { }
23
+ unicode : { } ,
24
+ which : { }
24
25
} )
25
26
26
27
module . exports = fundCmd
@@ -29,7 +30,7 @@ const usage = require('./utils/usage')
29
30
fundCmd . usage = usage (
30
31
'fund' ,
31
32
'npm fund [--json]' ,
32
- 'npm fund [--browser] [[<@scope>/]<pkg>'
33
+ 'npm fund [--browser] [[<@scope>/]<pkg> [--which=<fundingSourceNumber>] '
33
34
)
34
35
35
36
fundCmd . completion = function ( opts , cb ) {
@@ -52,96 +53,52 @@ function printJSON (fundingInfo) {
52
53
// level possible, in that process they also carry their dependencies along
53
54
// with them, moving those up in the visual tree
54
55
function printHuman ( fundingInfo , opts ) {
55
- // mapping logic that keeps track of seen items in order to be able
56
- // to push all other items from the same type/url in the same place
57
- const seen = new Map ( )
56
+ const flatCache = fundingInfo [ flatCacheSymbol ]
58
57
59
- function seenKey ( { type, url } = { } ) {
60
- return url ? String ( type ) + String ( url ) : null
61
- }
62
-
63
- function setStackedItem ( funding , result ) {
64
- const key = seenKey ( funding )
65
- if ( key && ! seen . has ( key ) ) seen . set ( key , result )
66
- }
58
+ const { name, version } = fundingInfo
59
+ const printableVersion = version ? `@${ version } ` : ''
67
60
68
- function retrieveStackedItem ( funding ) {
69
- const key = seenKey ( funding )
70
- if ( key && seen . has ( key ) ) return seen . get ( key )
71
- }
61
+ const items = Object . keys ( flatCache ) . map ( ( url ) => {
62
+ const deps = flatCache [ url ]
72
63
73
- // ---
74
-
75
- const getFundingItems = ( fundingItems ) =>
76
- Object . keys ( fundingItems || { } ) . map ( ( fundingItemName ) => {
77
- // first-level loop, prepare the pretty-printed formatted data
78
- const fundingItem = fundingItems [ fundingItemName ]
79
- const { version, funding } = fundingItem
80
- const { type, url } = funding || { }
64
+ const packages = deps . map ( ( dep ) => {
65
+ const { name, version } = dep
81
66
82
67
const printableVersion = version ? `@${ version } ` : ''
83
- const printableType = type && { label : `type: ${ funding . type } ` }
84
- const printableUrl = url && { label : `url: ${ funding . url } ` }
85
- const result = {
86
- fundingItem,
87
- label : fundingItemName + printableVersion ,
88
- nodes : [ ]
89
- }
90
-
91
- if ( printableType ) {
92
- result . nodes . push ( printableType )
93
- }
94
-
95
- if ( printableUrl ) {
96
- result . nodes . push ( printableUrl )
97
- }
98
-
99
- setStackedItem ( funding , result )
100
-
101
- return result
102
- } ) . reduce ( ( res , result ) => {
103
- // recurse and exclude nodes that are going to be stacked together
104
- const { fundingItem } = result
105
- const { dependencies, funding } = fundingItem
106
- const items = getFundingItems ( dependencies )
107
- const stackedResult = retrieveStackedItem ( funding )
108
- items . forEach ( i => result . nodes . push ( i ) )
109
-
110
- if ( stackedResult && stackedResult !== result ) {
111
- stackedResult . label += `, ${ result . label } `
112
- items . forEach ( i => stackedResult . nodes . push ( i ) )
113
- return res
114
- }
115
-
116
- res . push ( result )
117
-
118
- return res
119
- } , [ ] )
120
-
121
- const [ result ] = getFundingItems ( {
122
- [ fundingInfo . name ] : {
123
- dependencies : fundingInfo . dependencies ,
124
- funding : fundingInfo . funding ,
125
- version : fundingInfo . version
68
+ return `${ name } ${ printableVersion } `
69
+ } )
70
+
71
+ return {
72
+ label : url ,
73
+ nodes : [ packages . join ( ', ' ) ]
126
74
}
127
75
} )
128
76
129
- return archy ( result , '' , { unicode : opts . unicode } )
77
+ return archy ( { label : ` ${ name } ${ printableVersion } ` , nodes : items } , '' , { unicode : opts . unicode } )
130
78
}
131
79
132
- function openFundingUrl ( packageName , cb ) {
80
+ function openFundingUrl ( packageName , fundingSourceNumber , cb ) {
133
81
function getUrlAndOpen ( packageMetadata ) {
134
82
const { funding } = packageMetadata
135
- const { type, url } = retrieveFunding ( funding ) || { }
136
- const noFundingError =
137
- new Error ( `No funding method available for: ${ packageName } ` )
138
- noFundingError . code = 'ENOFUND'
139
- const typePrefix = type ? `${ type } funding` : 'Funding'
140
- const msg = `${ typePrefix } available at the following URL`
141
-
142
- if ( validFundingUrl ( funding ) ) {
83
+ const validSources = [ ] . concat ( retrieveFunding ( funding ) ) . filter ( validFundingField )
84
+
85
+ if ( validSources . length === 1 || ( fundingSourceNumber > 0 && fundingSourceNumber <= validSources . length ) ) {
86
+ const { type, url } = validSources [ fundingSourceNumber ? fundingSourceNumber - 1 : 0 ]
87
+ const typePrefix = type ? `${ type } funding` : 'Funding'
88
+ const msg = `${ typePrefix } available at the following URL`
143
89
openUrl ( url , msg , cb )
90
+ } else if ( ! ( fundingSourceNumber >= 1 ) ) {
91
+ validSources . forEach ( ( { type, url } , i ) => {
92
+ const typePrefix = type ? `${ type } funding` : 'Funding'
93
+ const msg = `${ typePrefix } available at the following URL`
94
+ console . log ( `${ i + 1 } : ${ msg } : ${ url } ` )
95
+ } )
96
+ console . log ( 'Run `npm fund [<@scope>/]<pkg> --which=1`, for example, to open the first funding URL listed in that package' )
97
+ cb ( )
144
98
} else {
99
+ const noFundingError = new Error ( `No valid funding method available for: ${ packageName } ` )
100
+ noFundingError . code = 'ENOFUND'
101
+
145
102
throw noFundingError
146
103
}
147
104
}
@@ -161,15 +118,24 @@ function fundCmd (args, cb) {
161
118
const opts = FundConfig ( npmConfig ( ) )
162
119
const dir = path . resolve ( npm . dir , '..' )
163
120
const packageName = args [ 0 ]
121
+ const numberArg = opts . which
122
+
123
+ const fundingSourceNumber = numberArg && parseInt ( numberArg , 10 )
124
+
125
+ if ( numberArg !== undefined && ( String ( fundingSourceNumber ) !== numberArg || fundingSourceNumber < 1 ) ) {
126
+ const err = new Error ( '`npm fund [<@scope>/]<pkg> [--which=fundingSourceNumber]` must be given a positive integer' )
127
+ err . code = 'EFUNDNUMBER'
128
+ throw err
129
+ }
164
130
165
131
if ( opts . global ) {
166
- const err = new Error ( '`npm fund` does not support globals ' )
132
+ const err = new Error ( '`npm fund` does not support global packages ' )
167
133
err . code = 'EFUNDGLOBAL'
168
134
throw err
169
135
}
170
136
171
137
if ( packageName ) {
172
- openFundingUrl ( packageName , cb )
138
+ openFundingUrl ( packageName , fundingSourceNumber , cb )
173
139
return
174
140
}
175
141
0 commit comments