11'use strict'
22
3- const path = require ( 'path' )
4-
53const archy = require ( 'archy' )
6- const readPackageTree = require ( 'read-package-tree' )
4+ const Arborist = require ( '@npmcli/arborist' )
5+ const pacote = require ( 'pacote' )
6+ const semver = require ( 'semver' )
7+ const npa = require ( 'npm-package-arg' )
8+ const { depth } = require ( 'treeverse' )
9+ const {
10+ readTree : getFundingInfo ,
11+ normalizeFunding,
12+ isValidFunding
13+ } = require ( 'libnpmfund' )
714
815const npm = require ( './npm.js' )
9- const fetchPackageMetadata = require ( './fetch-package-metadata.js' )
10- const computeMetadata = require ( './install/deps.js' ) . computeMetadata
11- const readShrinkwrap = require ( './install/read-shrinkwrap.js' )
12- const mutateIntoLogicalTree = require ( './install/mutate-into-logical-tree.js' )
1316const output = require ( './utils/output.js' )
1417const openUrl = require ( './utils/open-url.js' )
15- const {
16- getFundingInfo,
17- retrieveFunding,
18- validFundingUrl
19- } = require ( './utils/funding.js' )
18+ const usageUtil = require ( './utils/usage.js' )
2019
21- module . exports = fundCmd
22-
23- const usage = require ( './utils/usage' )
24- fundCmd . usage = usage (
20+ const usage = usageUtil (
2521 'fund' ,
26- 'npm fund [--json] ' ,
27- 'npm fund [--browser] [[ <@scope>/]<pkg>'
22+ 'npm fund' ,
23+ 'npm fund [--json] [-- browser] [--unicode] [[ <@scope>/]<pkg> [--which=<fundingSourceNumber>] '
2824)
2925
30- fundCmd . completion = function ( opts , cb ) {
26+ const completion = ( opts , cb ) => {
3127 const argv = opts . conf . argv . remain
3228 switch ( argv [ 2 ] ) {
3329 case 'fund' :
@@ -37,161 +33,190 @@ fundCmd.completion = function (opts, cb) {
3733 }
3834}
3935
36+ const cmd = ( args , cb ) => fund ( args ) . then ( ( ) => cb ( ) ) . catch ( cb )
37+
4038function printJSON ( fundingInfo ) {
4139 return JSON . stringify ( fundingInfo , null , 2 )
4240}
4341
44- // the human-printable version does some special things that turned out to
45- // be very verbose but hopefully not hard to follow: we stack up items
46- // that have a shared url/type and make sure they're printed at the highest
47- // level possible, in that process they also carry their dependencies along
48- // with them, moving those up in the visual tree
49- function printHuman ( fundingInfo , opts ) {
50- // mapping logic that keeps track of seen items in order to be able
51- // to push all other items from the same type/url in the same place
52- const seen = new Map ( )
53-
54- function seenKey ( { type, url } = { } ) {
55- return url ? String ( type ) + String ( url ) : null
56- }
57-
58- function setStackedItem ( funding , result ) {
59- const key = seenKey ( funding )
60- if ( key && ! seen . has ( key ) ) seen . set ( key , result )
61- }
62-
63- function retrieveStackedItem ( funding ) {
64- const key = seenKey ( funding )
65- if ( key && seen . has ( key ) ) return seen . get ( key )
66- }
42+ const getPrintableName = ( { name, version } ) => {
43+ const printableVersion = version ? `@${ version } ` : ''
44+ return `${ name } ${ printableVersion } `
45+ }
6746
68- // ---
69-
70- const getFundingItems = ( fundingItems ) =>
71- Object . keys ( fundingItems || { } ) . map ( ( fundingItemName ) => {
72- // first-level loop, prepare the pretty-printed formatted data
73- const fundingItem = fundingItems [ fundingItemName ]
74- const { version, funding } = fundingItem
75- const { type, url } = funding || { }
76-
77- const printableVersion = version ? `@${ version } ` : ''
78- const printableType = type && { label : `type: ${ funding . type } ` }
79- const printableUrl = url && { label : `url: ${ funding . url } ` }
80- const result = {
81- fundingItem,
82- label : fundingItemName + printableVersion ,
83- nodes : [ ]
47+ function printHuman ( fundingInfo , { unicode } ) {
48+ const seenUrls = new Map ( )
49+
50+ const tree = obj =>
51+ archy ( obj , '' , { unicode } )
52+
53+ const result = depth ( {
54+ tree : fundingInfo ,
55+ visit : ( { name, version, funding } ) => {
56+ // composes human readable package name
57+ // and creates a new archy item for readable output
58+ const { url } = funding || { }
59+ const pkgRef = getPrintableName ( { name, version } )
60+ const label = url ? tree ( {
61+ label : url ,
62+ nodes : [ pkgRef ]
63+ } ) . trim ( ) : pkgRef
64+ let item = {
65+ label
8466 }
8567
86- if ( printableType ) {
87- result . nodes . push ( printableType )
68+ // stacks all packages together under the same item
69+ if ( seenUrls . has ( url ) ) {
70+ item = seenUrls . get ( url )
71+ item . label += `, ${ pkgRef } `
72+ return null
73+ } else {
74+ seenUrls . set ( url , item )
8875 }
8976
90- if ( printableUrl ) {
91- result . nodes . push ( printableUrl )
92- }
77+ return item
78+ } ,
79+
80+ // puts child nodes back into returned archy
81+ // output while also filtering out missing items
82+ leave : ( item , children ) => {
83+ if ( item )
84+ item . nodes = children . filter ( Boolean )
85+
86+ return item
87+ } ,
88+
89+ // turns tree-like object return by libnpmfund
90+ // into children to be properly read by treeverse
91+ getChildren : ( node ) =>
92+ Object . keys ( node . dependencies || { } )
93+ . map ( key => ( {
94+ name : key ,
95+ ...node . dependencies [ key ]
96+ } ) )
97+ } )
98+
99+ return tree ( result )
100+ }
93101
94- setStackedItem ( funding , result )
95-
96- return result
97- } ) . reduce ( ( res , result ) => {
98- // recurse and exclude nodes that are going to be stacked together
99- const { fundingItem } = result
100- const { dependencies, funding } = fundingItem
101- const items = getFundingItems ( dependencies )
102- const stackedResult = retrieveStackedItem ( funding )
103- items . forEach ( i => result . nodes . push ( i ) )
104-
105- if ( stackedResult && stackedResult !== result ) {
106- stackedResult . label += `, ${ result . label } `
107- items . forEach ( i => stackedResult . nodes . push ( i ) )
108- return res
102+ async function openFundingUrl ( { path, tree, spec, fundingSourceNumber } ) {
103+ const arg = npa ( spec , path )
104+ const retrievePackageMetadata = ( ) => {
105+
106+ if ( arg . type === 'directory' ) {
107+ if ( tree . path === arg . fetchSpec ) {
108+ // matches cwd, e.g: npm fund .
109+ return tree . package
110+ } else {
111+ // matches any file path within current arborist inventory
112+ for ( const item of tree . inventory . values ( ) ) {
113+ if ( item . path === arg . fetchSpec ) {
114+ return item . package
115+ }
116+ }
117+ }
118+ } else {
119+ // tries to retrieve a package from arborist inventory
120+ // by matching resulted package name from the provided spec
121+ const [ item ] = [ ...tree . inventory . query ( 'name' , arg . name ) ]
122+ . filter ( i => semver . valid ( i . package . version ) )
123+ . sort ( ( a , b ) => semver . rcompare ( a . package . version , b . package . version ) )
124+
125+ if ( item ) {
126+ return item . package
109127 }
128+ }
129+ }
110130
111- res . push ( result )
131+ let { funding } = retrievePackageMetadata ( ) || { }
112132
113- return res
114- } , [ ] )
133+ if ( ! funding ) {
134+ // if still has not funding info, let's try
135+ // fetching metadata from the registry then
136+ const manifest = await pacote . manifest ( arg , npm . flatOptions )
137+ funding = manifest . funding
138+ }
115139
116- const [ result ] = getFundingItems ( {
117- [ fundingInfo . name ] : {
118- dependencies : fundingInfo . dependencies ,
119- funding : fundingInfo . funding ,
120- version : fundingInfo . version
121- }
122- } )
140+ const validSources = [ ]
141+ . concat ( normalizeFunding ( funding ) )
142+ . filter ( isValidFunding )
123143
124- return archy ( result , '' , { unicode : opts . unicode } )
125- }
144+ const matchesValidSource =
145+ validSources . length === 1 ||
146+ ( fundingSourceNumber > 0 && fundingSourceNumber <= validSources . length )
126147
127- function openFundingUrl ( packageName , cb ) {
128- function getUrlAndOpen ( packageMetadata ) {
129- const { funding } = packageMetadata
130- const { type, url } = retrieveFunding ( funding ) || { }
131- const noFundingError =
132- new Error ( `No funding method available for: ${ packageName } ` )
133- noFundingError . code = 'ENOFUND'
148+ if ( matchesValidSource ) {
149+ const index = fundingSourceNumber ? fundingSourceNumber - 1 : 0
150+ const { type, url } = validSources [ index ]
134151 const typePrefix = type ? `${ type } funding` : 'Funding'
135152 const msg = `${ typePrefix } available at the following URL`
153+ return new Promise ( ( resolve , reject ) =>
154+ openUrl ( url , msg , err => err
155+ ? reject ( err )
156+ : resolve ( )
157+ ) )
158+ } else if ( validSources . length && ! ( fundingSourceNumber >= 1 ) ) {
159+ validSources . forEach ( ( { type, url } , i ) => {
160+ const typePrefix = type ? `${ type } funding` : 'Funding'
161+ const msg = `${ typePrefix } available at the following URL`
162+ output ( `${ i + 1 } : ${ msg } : ${ url } ` )
163+ } )
164+ output ( 'Run `npm fund [<@scope>/]<pkg> --which=1`, for example, to open the first funding URL listed in that package' )
165+ } else {
166+ const noFundingError = new Error ( `No valid funding method available for: ${ spec } ` )
167+ noFundingError . code = 'ENOFUND'
136168
137- if ( validFundingUrl ( funding ) ) {
138- openUrl ( url , msg , cb )
139- } else {
140- throw noFundingError
141- }
169+ throw noFundingError
142170 }
143-
144- fetchPackageMetadata (
145- packageName ,
146- '.' ,
147- { fullMetadata : true } ,
148- function ( err , packageMetadata ) {
149- if ( err ) return cb ( err )
150- getUrlAndOpen ( packageMetadata )
151- }
152- )
153171}
154172
155- function fundCmd ( args , cb ) {
173+ const fund = async ( args ) => {
156174 const opts = npm . flatOptions
157- const dir = path . resolve ( npm . dir , '..' )
158- const packageName = args [ 0 ]
175+ const spec = args [ 0 ]
176+ const numberArg = opts . which
177+
178+ const fundingSourceNumber = numberArg && parseInt ( numberArg , 10 )
179+
180+ const badFundingSourceNumber =
181+ numberArg !== undefined &&
182+ ( String ( fundingSourceNumber ) !== numberArg || fundingSourceNumber < 1 )
183+
184+ if ( badFundingSourceNumber ) {
185+ const err = new Error ( '`npm fund [<@scope>/]<pkg> [--which=fundingSourceNumber]` must be given a positive integer' )
186+ err . code = 'EFUNDNUMBER'
187+ throw err
188+ }
159189
160190 if ( opts . global ) {
161- const err = new Error ( '`npm fund` does not support globals ' )
191+ const err = new Error ( '`npm fund` does not support global packages ' )
162192 err . code = 'EFUNDGLOBAL'
163193 throw err
164194 }
165195
166- if ( packageName ) {
167- openFundingUrl ( packageName , cb )
196+ const where = npm . prefix
197+ const arb = new Arborist ( { ...opts , path : where } )
198+ const tree = await arb . loadActual ( )
199+
200+ if ( spec ) {
201+ await openFundingUrl ( {
202+ path : where ,
203+ tree,
204+ spec,
205+ fundingSourceNumber
206+ } )
168207 return
169208 }
170209
171- readPackageTree ( dir , function ( err , tree ) {
172- if ( err ) {
173- process . exitCode = 1
174- return cb ( err )
175- }
210+ const print = opts . json
211+ ? printJSON
212+ : printHuman
176213
177- readShrinkwrap . andInflate ( tree , function ( ) {
178- const fundingInfo = getFundingInfo (
179- mutateIntoLogicalTree . asReadInstalled (
180- computeMetadata ( tree )
181- )
182- )
183-
184- const print = opts . json
185- ? printJSON
186- : printHuman
187-
188- output (
189- print (
190- fundingInfo ,
191- opts
192- )
193- )
194- cb ( err , tree )
195- } )
196- } )
214+ output (
215+ print (
216+ getFundingInfo ( tree ) ,
217+ opts
218+ )
219+ )
197220}
221+
222+ module . exports = Object . assign ( cmd , { usage, completion } )
0 commit comments