diff --git a/docs/content/cli-commands/npm-audit.md b/docs/content/cli-commands/npm-audit.md index 8e488614142d0..22b238a1f7159 100755 --- a/docs/content/cli-commands/npm-audit.md +++ b/docs/content/cli-commands/npm-audit.md @@ -8,7 +8,7 @@ description: Run a security audit ## Run a security audit -### Synposis +### Synopsis ```bash npm audit [--json|--parseable|--audit-level=(low|moderate|high|critical)] diff --git a/docs/content/cli-commands/npm-fund.md b/docs/content/cli-commands/npm-fund.md new file mode 100644 index 0000000000000..f6ad5a9fbf523 --- /dev/null +++ b/docs/content/cli-commands/npm-fund.md @@ -0,0 +1,60 @@ +--- +section: cli-commands +title: npm-fund +description: Retrieve funding information +--- + +# npm-fund + +## Retrieve funding information + +### Synopsis + +```bash + npm fund [] +``` + +### Description + +This command retrieves information on how to fund the dependencies of +a given project. If no package name is provided, it will list all +dependencies that are looking for funding in a tree-structure in which +are listed the type of funding and the url to visit. If a package name +is provided then it tries to open its funding url using the `--browser` +config param. + +The list will avoid duplicated entries and will stack all packages +that share the same type/url as a single entry. Given this nature the +list is not going to have the same shape of the output from `npm ls`. + +### Configuration + +#### browser + +* Default: OS X: `"open"`, Windows: `"start"`, Others: `"xdg-open"` +* Type: String + +The browser that is called by the `npm fund` command to open websites. + +#### json + +* Default: false +* Type: Boolean + +Show information in JSON format. + +#### unicode + +* Type: Boolean +* Default: true + +Whether to represent the tree structure using unicode characters. +Set it to `false` in order to use all-ansi output. + +## See Also + +* [npm-docs](/cli-commands/npm-docs) +* [npm-config](/cli-commands/npm-config) +* [npm-install](/cli-commands/npm-install) +* [npm-ls](/cli-commands/npm-ls) + diff --git a/docs/content/cli-commands/npm-install.md b/docs/content/cli-commands/npm-install.md index 684ab9ffa1b7f..523fd68f96a49 100755 --- a/docs/content/cli-commands/npm-install.md +++ b/docs/content/cli-commands/npm-install.md @@ -357,6 +357,10 @@ local copy exists on disk. npm install sax --force ``` +The `--no-fund` argument will hide the message displayed at the end of each +install that aknowledges the number of dependencies looking for funding. +See `npm-fund(1)` + The `-g` or `--global` argument will cause npm to install the package globally rather than locally. See [npm-folders](/docs/configuring-npm/folders). @@ -481,6 +485,7 @@ affects a real use-case, it will be investigated. * [npm folders](/configuring-npm/folders) * [npm update](/cli-commands/npm-update) * [npm audit](/cli-commands/npm-audit) +* [npm fund](/cli-commands/npm-fund) * [npm link](/cli-commands/npm-link) * [npm rebuild](/cli-commands/npm-rebuild) * [npm scripts](/using-npm/scripts) diff --git a/docs/content/cli-commands/npm-ls.md b/docs/content/cli-commands/npm-ls.md index 1eaa1bb92c4b3..703b319b40dae 100755 --- a/docs/content/cli-commands/npm-ls.md +++ b/docs/content/cli-commands/npm-ls.md @@ -109,6 +109,14 @@ When "prod" or "production", is an alias to `production`. Display only dependencies which are linked +#### unicode + +* Type: Boolean +* Default: true + +Whether to represent the tree structure using unicode characters. +Set it to false in order to use all-ansi output. + ### See Also * [npm config](/cli-commands/npm-config) diff --git a/docs/content/configuring-npm/package-json.md b/docs/content/configuring-npm/package-json.md index f17bd6d83e80b..6c19b94ba3515 100755 --- a/docs/content/configuring-npm/package-json.md +++ b/docs/content/configuring-npm/package-json.md @@ -194,15 +194,25 @@ Both email and url are optional either way. npm also sets a top-level "maintainers" field with your npm user info. -### support +### funding -You can specify a URL for up-to-date information about ways to support -development of your package: +You can specify an object containing an URL that provides up-to-date +information about ways to help fund development of your package: - { "support": "https://example.com/project/support" } + "funding": { + "type" : "individual", + "url" : "http://example.com/donate" + } + + "funding": { + "type" : "patreon", + "url" : "https://www.patreon.com/my-account" + } -Users can use the `npm support` subcommand to list the `support` URLs -of all dependencies of the project, direct and indirect. +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 `. ### files diff --git a/docs/content/using-npm/config.md b/docs/content/using-npm/config.md index 4a0fd5f07199a..b5b43710021d6 100755 --- a/docs/content/using-npm/config.md +++ b/docs/content/using-npm/config.md @@ -449,6 +449,15 @@ packages. The "maxTimeout" config for the `retry` module to use when fetching packages. +#### fund + +* Default: true +* Type: Boolean + +When "true" displays the message at the end of each `npm install` +aknowledging the number of dependencies looking for funding. +See [`npm-fund`](/docs/cli-commands/npm-fund) for details. + #### git * Default: `"git"` diff --git a/lib/config/cmd-list.js b/lib/config/cmd-list.js index c11c8c30982b8..d9d0d85b7d520 100644 --- a/lib/config/cmd-list.js +++ b/lib/config/cmd-list.js @@ -91,7 +91,7 @@ var cmdList = [ 'token', 'profile', 'audit', - 'support', + 'fund', 'org', 'help', diff --git a/lib/config/defaults.js b/lib/config/defaults.js index 57d373df1e10c..e07da3aaf97f4 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -143,6 +143,8 @@ Object.defineProperty(exports, 'defaults', {get: function () { force: false, 'format-package-lock': true, + fund: true, + 'fetch-retries': 2, 'fetch-retry-factor': 10, 'fetch-retry-mintimeout': 10000, @@ -284,6 +286,7 @@ exports.types = { editor: String, 'engine-strict': Boolean, force: Boolean, + fund: Boolean, 'format-package-lock': Boolean, 'fetch-retries': Number, 'fetch-retry-factor': Number, diff --git a/lib/fund.js b/lib/fund.js new file mode 100644 index 0000000000000..4981e461596c0 --- /dev/null +++ b/lib/fund.js @@ -0,0 +1,202 @@ +'use strict' + +const path = require('path') + +const archy = require('archy') +const figgyPudding = require('figgy-pudding') +const readPackageTree = require('read-package-tree') + +const npm = require('./npm.js') +const npmConfig = require('./config/figgy-config.js') +const fetchPackageMetadata = require('./fetch-package-metadata.js') +const computeMetadata = require('./install/deps.js').computeMetadata +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, validFundingUrl } = require('./utils/funding.js') + +const FundConfig = figgyPudding({ + browser: {}, // used by ./utils/open-url + global: {}, + json: {}, + unicode: {} +}) + +module.exports = fundCmd + +const usage = require('./utils/usage') +fundCmd.usage = usage( + 'fund', + 'npm fund [--json]', + 'npm fund [--browser] [[<@scope>/]' +) + +fundCmd.completion = function (opts, cb) { + const argv = opts.conf.argv.remain + switch (argv[2]) { + case 'fund': + return cb(null, []) + default: + return cb(new Error(argv[2] + ' not recognized')) + } +} + +function printJSON (fundingInfo) { + return JSON.stringify(fundingInfo, null, 2) +} + +// the human-printable version does some special things that turned out to +// be very verbose but hopefully not hard to follow: we stack up items +// that have a shared url/type and make sure they're printed at the highest +// 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() + + 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 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 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 archy(result, '', { unicode: opts.unicode }) +} + +function openFundingUrl (packageName, cb) { + function getUrlAndOpen (packageMetadata) { + const { funding } = packageMetadata + const { type, url } = 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)) { + openUrl(url, msg, cb) + } else { + throw noFundingError + } + } + + fetchPackageMetadata( + packageName, + '.', + { fullMetadata: true }, + function (err, packageMetadata) { + if (err) return cb(err) + getUrlAndOpen(packageMetadata) + } + ) +} + +function fundCmd (args, cb) { + const opts = FundConfig(npmConfig()) + const dir = path.resolve(npm.dir, '..') + const packageName = args[0] + + if (opts.global) { + const err = new Error('`npm fund` does not support globals') + err.code = 'EFUNDGLOBAL' + throw err + } + + if (packageName) { + openFundingUrl(packageName, cb) + return + } + + readPackageTree(dir, function (err, tree) { + if (err) { + process.exitCode = 1 + return cb(err) + } + + readShrinkwrap.andInflate(tree, function () { + const fundingInfo = getFundingInfo( + mutateIntoLogicalTree.asReadInstalled( + computeMetadata(tree) + ) + ) + + const print = opts.json + ? printJSON + : printHuman + + output( + print( + fundingInfo, + opts + ) + ) + cb(err, tree) + }) + }) +} diff --git a/lib/install.js b/lib/install.js index 52fe96c47f5ff..a4cf2b186de51 100644 --- a/lib/install.js +++ b/lib/install.js @@ -119,7 +119,6 @@ var unlock = locker.unlock var parseJSON = require('./utils/parse-json.js') var output = require('./utils/output.js') var saveMetrics = require('./utils/metrics.js').save -var validSupportURL = require('./utils/valid-support-url') // install specific libraries var copyTree = require('./install/copy-tree.js') @@ -139,6 +138,10 @@ var validateArgs = require('./install/validate-args.js') var saveRequested = require('./install/save.js').saveRequested var saveShrinkwrap = require('./install/save.js').saveShrinkwrap var audit = require('./install/audit.js') +var { + getPrintFundingReport, + getPrintFundingReportJSON +} = require('./install/fund.js') var getSaveType = require('./install/save.js').getSaveType var doSerialActions = require('./install/actions.js').doSerial var doReverseSerialActions = require('./install/actions.js').doReverseSerial @@ -241,6 +244,7 @@ function Installer (where, dryrun, args, opts) { this.saveOnlyLock = opts.saveOnlyLock this.global = opts.global != null ? opts.global : this.where === path.resolve(npm.globalDir, '..') this.audit = npm.config.get('audit') && !this.global + this.fund = npm.config.get('fund') && !this.global this.started = Date.now() } Installer.prototype = {} @@ -803,8 +807,6 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) { var added = 0 var updated = 0 var moved = 0 - // Check if any installed packages have support properties. - var haveSupportable = false // Count the number of contributors to packages added, tracking // contributors we've seen, so we can produce a running unique count. var contributors = new Set() @@ -812,11 +814,6 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) { var mutation = action[0] var pkg = action[1] if (pkg.failed) return - if ( - mutation !== 'remove' && validSupportURL(pkg.package.support) - ) { - haveSupportable = true - } if (mutation === 'remove') { ++removed } else if (mutation === 'move') { @@ -880,9 +877,6 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) { report += ' in ' + ((Date.now() - this.started) / 1000) + 's' output(report) - if (haveSupportable) { - output('Run `npm support` to support projects you depend on.') - } if (auditResult) { audit.printInstallReport(auditResult) } @@ -907,9 +901,23 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) { if (argument.url) returned += ' (' + argument.email + ')' return returned } + + const { fund, idealTree } = this + const printFundingReport = getPrintFundingReport({ + fund, + idealTree + }) + if (printFundingReport.length) { + output(printFundingReport) + } } Installer.prototype.printInstalledForJSON = function (diffs, auditResult) { + const { fund, idealTree } = this + const printFundingReport = getPrintFundingReportJSON({ + fund, + idealTree + }) var result = { added: [], removed: [], @@ -918,6 +926,7 @@ Installer.prototype.printInstalledForJSON = function (diffs, auditResult) { failed: [], warnings: [], audit: auditResult, + funding: printFundingReport, elapsed: Date.now() - this.started } var self = this diff --git a/lib/install/fund.js b/lib/install/fund.js new file mode 100644 index 0000000000000..55a167a95583a --- /dev/null +++ b/lib/install/fund.js @@ -0,0 +1,48 @@ +'use strict' + +const { EOL } = require('os') + +const computeMetadata = require('./deps.js').computeMetadata +const mutateIntoLogicalTree = require('./mutate-into-logical-tree.js') +var { getFundingInfo } = require('../utils/funding.js') + +exports.getPrintFundingReport = getPrintFundingReport +exports.getPrintFundingReportJSON = getPrintFundingReportJSON + +function getFundingResult ({ fund, idealTree }) { + if (fund) { + const fundingInfoTree = + mutateIntoLogicalTree.asReadInstalled( + computeMetadata(idealTree) + ) + const fundResult = getFundingInfo(fundingInfoTree, { countOnly: true }) + return fundResult + } else { + return {} + } +} + +function getPrintFundingReport ({ fund, idealTree }, opts) { + const fundResult = getFundingResult({ fund, idealTree }) + const { length } = fundResult || {} + const { json } = opts || {} + + function padding (msg) { + return json ? '' : (EOL + msg) + } + + function packageQuantity (amount) { + return `package${amount > 1 ? 's are' : ' is'}` + } + + if (!length) return '' + + return padding('') + length + ' ' + + packageQuantity(length) + + ' looking for funding.' + + padding('Run "npm fund" to find out more.') +} + +function getPrintFundingReportJSON ({ fund, idealTree }) { + return getPrintFundingReport({ fund, idealTree }, { json: true }) +} diff --git a/lib/support.js b/lib/support.js deleted file mode 100644 index 5813df93ff2a6..0000000000000 --- a/lib/support.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict' - -const npm = require('./npm.js') -const output = require('./utils/output.js') -const path = require('path') -const readPackageTree = require('read-package-tree') -const semver = require('semver') -const validSupportURL = require('./utils/valid-support-url') - -module.exports = support - -const usage = require('./utils/usage') -support.usage = usage( - 'support', - '\nnpm support [--json]' -) - -support.completion = function (opts, cb) { - const argv = opts.conf.argv.remain - switch (argv[2]) { - case 'support': - return cb(null, []) - default: - return cb(new Error(argv[2] + ' not recognized')) - } -} - -// Compare lib/ls.js. -function support (args, silent, cb) { - if (typeof cb !== 'function') { - cb = silent - silent = false - } - const dir = path.resolve(npm.dir, '..') - readPackageTree(dir, function (err, tree) { - if (err) { - process.exitCode = 1 - return cb(err) - } - const data = findPackages(tree) - if (silent) return cb(null, data) - var out - if (npm.config.get('json')) { - out = JSON.stringify(data, null, 2) - } else { - out = data.map(displayPackage).join('\n\n') - } - output(out) - cb(err, data) - }) -} - -function findPackages (root) { - const set = new Set() - iterate(root) - return Array.from(set).sort(function (a, b) { - const comparison = a.name - .toLowerCase() - .localeCompare(b.name.toLowerCase()) - return comparison === 0 - ? semver.compare(a.version, b.version) - : comparison - }) - - function iterate (node) { - node.children.forEach(recurse) - } - - function recurse (node) { - const metadata = node.package - const support = metadata.support - if (support && validSupportURL(support)) { - set.add({ - name: metadata.name, - version: metadata.version, - path: node.path, - homepage: metadata.homepage, - repository: metadata.repository, - support: metadata.support - }) - } - if (node.children) iterate(node) - } -} - -function displayPackage (entry) { - return entry.name + '@' + entry.version + ': ' + entry.support -} diff --git a/lib/utils/funding.js b/lib/utils/funding.js new file mode 100644 index 0000000000000..2c994e0b6b426 --- /dev/null +++ b/lib/utils/funding.js @@ -0,0 +1,145 @@ +'use strict' + +const URL = require('url').URL + +exports.getFundingInfo = getFundingInfo +exports.validFundingUrl = validFundingUrl + +// Is the value of a `funding` property of a `package.json` +// a valid type+url for `npm fund` to display? +function validFundingUrl (funding) { + if (!funding || !funding.url) { + return false + } + + try { + var parsed = new URL(funding.url) + } catch (error) { + return false + } + + if ( + parsed.protocol !== 'https:' && + parsed.protocol !== 'http:' + ) return false + + return Boolean(parsed.host) +} + +function getFundingInfo (idealTree, opts) { + let length = 0 + const seen = new Set() + const { countOnly } = opts || {} + const empty = () => Object.create(null) + const _trailingDependencies = Symbol('trailingDependencies') + + function tracked (name, version) { + const key = String(name) + String(version) + if (seen.has(key)) { + return true + } + seen.add(key) + } + + function retrieveDependencies (dependencies) { + const trailing = dependencies[_trailingDependencies] + + if (trailing) { + return Object.assign( + empty(), + dependencies, + trailing + ) + } + + return dependencies + } + + function hasDependencies (dependencies) { + return dependencies && ( + Object.keys(dependencies).length || + dependencies[_trailingDependencies] + ) + } + + function getFundingDependencies (tree) { + const deps = tree && tree.dependencies + if (!deps) return empty() + + // broken into two steps to make sure items appearance + // within top levels takes precedence over nested ones + return (Object.keys(deps)).map((key) => { + const dep = deps[key] + const { name, funding, version } = dep + + const fundingItem = {} + + // avoids duplicated items within the funding tree + if (tracked(name, version)) return empty() + + if (version) { + fundingItem.version = version + } + + if (funding && validFundingUrl(funding)) { + fundingItem.funding = funding + length++ + } + + return { + dep, + fundingItem + } + }).reduce((res, { dep, fundingItem }, i) => { + if (!fundingItem) return res + + // recurse + const dependencies = dep.dependencies && + Object.keys(dep.dependencies).length > 0 && + getFundingDependencies(dep) + + // if we're only counting items there's no need + // to add all the data to the resulting object + if (countOnly) return null + + if (hasDependencies(dependencies)) { + fundingItem.dependencies = retrieveDependencies(dependencies) + } + + if (fundingItem.funding) { + res[dep.name] = fundingItem + } else if (fundingItem.dependencies) { + res[_trailingDependencies] = + Object.assign( + empty(), + res[_trailingDependencies], + fundingItem.dependencies + ) + } + + return res + }, empty()) + } + + const idealTreeDependencies = getFundingDependencies(idealTree) + const result = { + length + } + + if (!countOnly) { + result.name = idealTree.name || idealTree.path + + if (idealTree && idealTree.version) { + result.version = idealTree.version + } + + if (idealTree && idealTree.funding) { + result.funding = idealTree.funding + } + + result.dependencies = + retrieveDependencies(idealTreeDependencies) + } + + return result +} diff --git a/lib/utils/open-url.js b/lib/utils/open-url.js index 7a48d2e868959..e1ed2b3fab76d 100644 --- a/lib/utils/open-url.js +++ b/lib/utils/open-url.js @@ -5,9 +5,28 @@ const opener = require('opener') // attempt to open URL in web-browser, print address otherwise: module.exports = function open (url, errMsg, cb, browser = npm.config.get('browser')) { - opener(url, { command: npm.config.get('browser') }, (er) => { + function printAlternateMsg () { + const json = npm.config.get('json') + const alternateMsg = json + ? JSON.stringify({ + title: errMsg, + url + }, null, 2) + : `${errMsg}:\n\n${url}` + + output(alternateMsg) + } + + const skipBrowser = process.argv.indexOf('--no-browser') > -1 + + if (skipBrowser) { + printAlternateMsg() + return cb() + } + + opener(url, { command: browser }, (er) => { if (er && er.code === 'ENOENT') { - output(`${errMsg}:\n\n${url}`) + printAlternateMsg() return cb() } else { return cb(er) diff --git a/lib/utils/valid-support-url.js b/lib/utils/valid-support-url.js deleted file mode 100644 index d575dcdf03b52..0000000000000 --- a/lib/utils/valid-support-url.js +++ /dev/null @@ -1,19 +0,0 @@ -const URL = require('url').URL - -// Is the value of a `support` property of a `package.json` object -// a valid URL for `npm support` to display? -module.exports = function (argument) { - if (typeof argument !== 'string' || argument.length === 0) { - return false - } - try { - var parsed = new URL(argument) - } catch (error) { - return false - } - if ( - parsed.protocol !== 'https:' && - parsed.protocol !== 'http:' - ) return false - return parsed.host -} diff --git a/package-lock.json b/package-lock.json index 5dd3a858aefa8..125f37f22e6c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -438,17 +438,6 @@ "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" - }, - "dependencies": { - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } } }, "supports-color": { @@ -2437,6 +2426,15 @@ "function-bind": "^1.1.1" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", diff --git a/tap-snapshots/test-tap-fund.js-TAP.test.js b/tap-snapshots/test-tap-fund.js-TAP.test.js new file mode 100644 index 0000000000000..e351a21c66919 --- /dev/null +++ b/tap-snapshots/test-tap-fund.js-TAP.test.js @@ -0,0 +1,54 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/tap/fund.js TAP fund containing multi-level nested deps with no funding > should omit dependencies with no funding declared 1`] = ` +nested-no-funding-packages@1.0.0 ++-- lorem@1.0.0 +| \`-- url: https://example.com/lorem +\`-- bar@1.0.0 + +-- type: individual + +-- url: http://example.com/donate + \`-- sub-bar@1.0.0 + \`-- url: https://example.com/sponsor + + +` + +exports[`test/tap/fund.js TAP fund does not support global > should throw EFUNDGLOBAL error 1`] = ` + +` + +exports[`test/tap/fund.js TAP fund does not support global > should write error msgs to stderr 1`] = ` +npm ERR! code EFUNDGLOBAL +npm ERR! \`npm fund\` does not support globals +` + +exports[`test/tap/fund.js TAP fund does not support global, using --json option > should write error msgs to stderr 1`] = ` +npm ERR! code EFUNDGLOBAL +npm ERR! \`npm fund\` does not support globals +` + +exports[`test/tap/fund.js TAP fund in which same maintainer owns all its deps > should print stack packages together 1`] = ` +maintainer-owns-all-deps@1.0.0, dep-bar@1.0.0, dep-sub-foo@1.0.0, dep-foo@1.0.0 ++-- type: individual +\`-- url: http://example.com/donate + + +` + +exports[`test/tap/fund.js TAP fund using package argument with no browser > should open funding url 1`] = ` +individual funding available at the following URL: + +http://example.com/donate + +` + +exports[`test/tap/fund.js TAP fund with no package containing funding > should print empty funding info 1`] = ` +no-funding-package@0.0.0 + + +` diff --git a/tap-snapshots/test-tap-repo.js-TAP.test.js b/tap-snapshots/test-tap-repo.js-TAP.test.js new file mode 100644 index 0000000000000..3fba79edb8d42 --- /dev/null +++ b/tap-snapshots/test-tap-repo.js-TAP.test.js @@ -0,0 +1,21 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/tap/repo.js TAP npm repo underscore --json > should print json result 1`] = ` +{ + "title": "repository available at the following URL", + "url": "https://github.com/jashkenas/underscore" +} + +` + +exports[`test/tap/repo.js TAP npm repo underscore --no-browser > should print alternative msg 1`] = ` +repository available at the following URL: + +https://github.com/jashkenas/underscore + +` diff --git a/test/tap/fund.js b/test/tap/fund.js new file mode 100644 index 0000000000000..cc66bea51a056 --- /dev/null +++ b/test/tap/fund.js @@ -0,0 +1,298 @@ +'use strict' + +const fs = require('fs') +const path = require('path') + +const test = require('tap').test +const Tacks = require('tacks') +const Dir = Tacks.Dir +const File = Tacks.File +const common = require('../common-tap.js') +const isWindows = require('../../lib/utils/is-windows.js') + +const base = common.pkg +const noFunding = path.join(base, 'no-funding-package') +const maintainerOwnsAllDeps = path.join(base, 'maintainer-owns-all-deps') +const nestedNoFundingPackages = path.join(base, 'nested-no-funding-packages') + +function getFixturePackage ({ name, version, dependencies, funding }, extras) { + const getDeps = () => Object + .keys(dependencies) + .reduce((res, dep) => (Object.assign({}, res, { + [dep]: '*' + })), {}) + + return Dir(Object.assign({ + 'package.json': File({ + name, + version: version || '1.0.0', + funding: (funding === undefined) ? { + type: 'individual', + url: 'http://example.com/donate' + } : funding, + dependencies: dependencies && getDeps(dependencies) + }) + }, extras)) +} + +const fixture = new Tacks(Dir({ + 'no-funding-package': Dir({ + 'package.json': File({ + name: 'no-funding-package', + version: '0.0.0' + }) + }), + 'maintainer-owns-all-deps': getFixturePackage({ + name: 'maintainer-owns-all-deps', + dependencies: { + 'dep-foo': '*', + 'dep-bar': '*' + } + }, { + node_modules: Dir({ + 'dep-foo': getFixturePackage({ + name: 'dep-foo', + dependencies: { + 'dep-sub-foo': '*' + } + }, { + node_modules: Dir({ + 'dep-sub-foo': getFixturePackage({ + name: 'dep-sub-foo' + }) + }) + }), + 'dep-bar': getFixturePackage({ + name: 'dep-bar' + }) + }) + }), + 'nested-no-funding-packages': getFixturePackage({ + name: 'nested-no-funding-packages', + funding: null, + dependencies: { + foo: '*' + }, + devDependencies: { + lorem: '*' + } + }, { + node_modules: Dir({ + foo: getFixturePackage({ + name: 'foo', + dependencies: { + bar: '*' + }, + funding: null + }, { + node_modules: Dir({ + bar: getFixturePackage({ + name: 'bar' + }, { + node_modules: Dir({ + 'sub-bar': getFixturePackage({ + name: 'sub-bar', + funding: { + url: 'https://example.com/sponsor' + } + }) + }) + }) + }) + }), + lorem: getFixturePackage({ + name: 'lorem', + funding: { + url: 'https://example.com/lorem' + } + }) + }) + }) +})) + +function checkOutput (t, { code, stdout, stderr }) { + t.is(code, 0, `exited code 0`) + t.is(stderr, '', 'no warnings') +} + +function jsonTest (t, { assertionMsg, expected, stdout }) { + let parsed = JSON.parse(stdout) + t.deepEqual(parsed, expected, assertionMsg) +} + +function snapshotTest (t, { stdout, assertionMsg }) { + t.matchSnapshot(stdout, assertionMsg) +} + +function testFundCmd ({ title, assertionMsg, args = [], opts = {}, output = checkOutput, assertion = snapshotTest, expected }) { + const validate = (t) => (err, code, stdout, stderr) => { + if (err) throw err + + output(t, { code, stdout, stderr }) + assertion(t, { assertionMsg, expected, stdout }) + } + + return test(title, (t) => { + t.plan(3) + common.npm(['fund', '--unicode=false'].concat(args), opts, validate(t)) + }) +} + +test('setup', function (t) { + fixture.remove(base) + fixture.create(base) + t.end() +}) + +testFundCmd({ + title: 'fund with no package containing funding', + assertionMsg: 'should print empty funding info', + opts: { cwd: noFunding } +}) + +testFundCmd({ + title: 'fund in which same maintainer owns all its deps', + assertionMsg: 'should print stack packages together', + opts: { cwd: maintainerOwnsAllDeps } +}) + +testFundCmd({ + title: 'fund in which same maintainer owns all its deps, using --json option', + assertionMsg: 'should print stack packages together', + args: ['--json'], + opts: { cwd: maintainerOwnsAllDeps }, + assertion: jsonTest, + expected: { + length: 3, + name: 'maintainer-owns-all-deps', + version: '1.0.0', + funding: { type: 'individual', url: 'http://example.com/donate' }, + dependencies: { + 'dep-bar': { + version: '1.0.0', + funding: { type: 'individual', url: 'http://example.com/donate' } + }, + 'dep-foo': { + version: '1.0.0', + funding: { type: 'individual', url: 'http://example.com/donate' }, + dependencies: { + 'dep-sub-foo': { + version: '1.0.0', + funding: { type: 'individual', url: 'http://example.com/donate' } + } + } + } + } + } +}) + +testFundCmd({ + title: 'fund containing multi-level nested deps with no funding', + assertionMsg: 'should omit dependencies with no funding declared', + opts: { cwd: nestedNoFundingPackages } +}) + +testFundCmd({ + title: 'fund containing multi-level nested deps with no funding, using --json option', + assertionMsg: 'should omit dependencies with no funding declared', + args: ['--json'], + opts: { cwd: nestedNoFundingPackages }, + assertion: jsonTest, + expected: { + length: 3, + name: 'nested-no-funding-packages', + version: '1.0.0', + dependencies: { + lorem: { version: '1.0.0', funding: { url: 'https://example.com/lorem' } }, + bar: { + version: '1.0.0', + funding: { type: 'individual', url: 'http://example.com/donate' }, + dependencies: { + 'sub-bar': { + version: '1.0.0', + funding: { url: 'https://example.com/sponsor' } + } + } + } + } + } +}) + +testFundCmd({ + title: 'fund does not support global', + assertionMsg: 'should throw EFUNDGLOBAL error', + args: ['--global'], + output: (t, { code, stdout, stderr }) => { + t.is(code, 1, `exited code 0`) + const [ errCode, errCmd ] = stderr.split('\n') + t.matchSnapshot(`${errCode}\n${errCmd}`, 'should write error msgs to stderr') + } +}) + +testFundCmd({ + title: 'fund does not support global, using --json option', + assertionMsg: 'should throw EFUNDGLOBAL error', + args: ['--global', '--json'], + output: (t, { code, stdout, stderr }) => { + t.is(code, 1, `exited code 0`) + const [ errCode, errCmd ] = stderr.split('\n') + t.matchSnapshot(`${errCode}\n${errCmd}`, 'should write error msgs to stderr') + }, + assertion: jsonTest, + expected: { + error: { + code: 'EFUNDGLOBAL', + summary: '`npm fund` does not support globals', + detail: '' + } + } +}) + +testFundCmd({ + title: 'fund using package argument with no browser', + assertionMsg: 'should open funding url', + args: ['.', '--no-browser'], + opts: { cwd: maintainerOwnsAllDeps } +}) + +testFundCmd({ + title: 'fund using package argument with no browser, using --json option', + assertionMsg: 'should open funding url', + args: ['.', '--json', '--no-browser'], + opts: { cwd: maintainerOwnsAllDeps }, + assertion: jsonTest, + expected: { + title: 'individual funding available at the following URL', + url: 'http://example.com/donate' + } +}) + +if (!isWindows) { + test('fund using package argument', function (t) { + const fakeBrowser = path.join(common.pkg, '_script.sh') + const outFile = path.join(common.pkg, '_output') + + const s = '#!/usr/bin/env bash\n' + + 'echo "$@" > ' + JSON.stringify(common.pkg) + '/_output\n' + fs.writeFileSync(fakeBrowser, s) + fs.chmodSync(fakeBrowser, '0755') + + common.npm([ + 'fund', '.', + '--loglevel=silent', + '--browser=' + fakeBrowser + ], { cwd: maintainerOwnsAllDeps }, function (err, code, stdout, stderr) { + t.ifError(err, 'repo command ran without error') + t.equal(code, 0, 'exit ok') + var res = fs.readFileSync(outFile, 'utf8') + t.equal(res, 'http://example.com/donate\n') + t.end() + }) + }) +} + +test('cleanup', function (t) { + t.pass(base) + fixture.remove(base) + t.end() +}) diff --git a/test/tap/install-mention-funding.js b/test/tap/install-mention-funding.js new file mode 100644 index 0000000000000..ebd25a57324c1 --- /dev/null +++ b/test/tap/install-mention-funding.js @@ -0,0 +1,127 @@ +'use strict' +const path = require('path') +const test = require('tap').test +const Tacks = require('tacks') +const Dir = Tacks.Dir +const File = Tacks.File +const common = require('../common-tap.js') + +const base = common.pkg +const singlePackage = path.join(base, 'single-funding-package') +const multiplePackages = path.join(base, 'top-level-funding') + +function getFixturePackage ({ name, version, dependencies, funding }) { + return Dir({ + 'package.json': File({ + name, + version: version || '1.0.0', + funding: funding || { + type: 'individual', + url: 'http://example.com/donate' + }, + dependencies: dependencies || {} + }) + }) +} + +const fixture = new Tacks(Dir({ + 'package.json': File({}), + 'single-funding-package': getFixturePackage({ + name: 'single-funding-package' + }), + 'top-level-funding': getFixturePackage({ + name: 'top-level-funding', + dependencies: { + 'dep-foo': 'file:../dep-foo', + 'dep-bar': 'file:../dep-bar' + } + }), + 'dep-foo': getFixturePackage({ + name: 'dep-foo', + funding: { + type: 'corporate', + url: 'https://corp.example.com/sponsor' + }, + dependencies: { + 'sub-dep-bar': 'file:../sub-dep-bar' + } + }), + 'dep-bar': getFixturePackage({ + name: 'dep-bar', + version: '2.1.0', + dependencies: { + 'sub-dep-bar': 'file:../sub-dep-bar' + } + }), + 'sub-dep-bar': getFixturePackage({ + name: 'sub-dep-bar', + funding: { + type: 'foo', + url: 'http://example.com/foo' + } + }) +})) + +test('mention npm fund upon installing single dependency', function (t) { + setup(t) + common.npm(['install', '--no-save', singlePackage], {cwd: base}, function (err, code, stdout, stderr) { + if (err) throw err + t.is(code, 0, 'installed successfully') + t.is(stderr, '', 'no warnings') + t.includes(stdout, '1 package is looking for funding.', 'should print amount of packages needing funding') + t.includes(stdout, 'Run "npm fund" to find out more.', 'should print npm fund mention') + t.end() + }) +}) + +test('mention npm fund upon installing multiple dependencies', function (t) { + setup(t) + common.npm(['install', '--no-save', multiplePackages], {cwd: base}, function (err, code, stdout, stderr) { + if (err) throw err + t.is(code, 0, 'installed successfully') + t.is(stderr, '', 'no warnings') + t.includes(stdout, '4 packages are looking for funding.', 'should print amount of packages needing funding') + t.includes(stdout, 'Run "npm fund" to find out more.', 'should print npm fund mention') + t.end() + }) +}) + +test('skips mention npm fund using --no-fund option', function (t) { + setup(t) + common.npm(['install', '--no-save', '--no-fund', multiplePackages], {cwd: base}, function (err, code, stdout, stderr) { + if (err) throw err + t.is(code, 0, 'installed successfully') + t.is(stderr, '', 'no warnings') + t.doesNotHave(stdout, '4 packages are looking for funding.', 'should print amount of packages needing funding') + t.doesNotHave(stdout, 'Run "npm fund" to find out more.', 'should print npm fund mention') + t.end() + }) +}) + +test('mention packages looking for funding using --json', function (t) { + setup(t) + common.npm(['install', '--no-save', '--json', multiplePackages], {cwd: base}, function (err, code, stdout, stderr) { + if (err) throw err + t.is(code, 0, 'installed successfully') + t.is(stderr, '', 'no warnings') + const res = JSON.parse(stdout) + t.match(res.funding, '4 packages are looking for funding.', 'should print amount of packages needing funding') + t.end() + }) +}) + +test('cleanup', function (t) { + cleanup() + t.end() +}) + +function setup (t) { + fixture.create(base) + t.teardown(() => { + cleanup() + }) +} + +function cleanup () { + fixture.remove(base) +} diff --git a/test/tap/install-mention-support.js b/test/tap/install-mention-support.js deleted file mode 100644 index a1cb2c4aaf7d7..0000000000000 --- a/test/tap/install-mention-support.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict' -var test = require('tap').test -var Tacks = require('tacks') -var Dir = Tacks.Dir -var File = Tacks.File -var common = require('../common-tap.js') - -var fixturepath = common.pkg -var fixture = new Tacks(Dir({ - 'package.json': File({}), - 'hassupport': Dir({ - 'package.json': File({ - name: 'hassupport', - version: '7.7.7', - support: 'http://example.com/project/support' - }) - }) -})) - -test('setup', function (t) { - fixture.remove(fixturepath) - fixture.create(fixturepath) - t.end() -}) - -test('install-report', function (t) { - common.npm(['install', '--no-save', './hassupport'], {cwd: fixturepath}, function (err, code, stdout, stderr) { - if (err) throw err - t.is(code, 0, 'installed successfully') - t.is(stderr, '', 'no warnings') - t.includes(stdout, '`npm support`', 'mentions `npm support`') - t.end() - }) -}) - -test('cleanup', function (t) { - fixture.remove(fixturepath) - t.end() -}) diff --git a/test/tap/install.fund.js b/test/tap/install.fund.js new file mode 100644 index 0000000000000..37a61e42891af --- /dev/null +++ b/test/tap/install.fund.js @@ -0,0 +1,100 @@ +'use strict' + +const { EOL } = require('os') +const { test } = require('tap') +const { getPrintFundingReport } = require('../../lib/install/fund') + +test('message when there are no funding found', (t) => { + t.deepEqual( + getPrintFundingReport({}), + '', + 'should not print any message if missing info' + ) + t.deepEqual( + getPrintFundingReport({ + name: 'foo', + version: '1.0.0', + dependencies: {} + }), + '', + 'should not print any message if package has no dependencies' + ) + t.deepEqual( + getPrintFundingReport({ + fund: true, + idealTree: { + name: 'foo', + version: '1.0.0', + dependencies: { + bar: {}, + lorem: {} + } + } + }), + '', + 'should not print any message if no package has funding info' + ) + t.end() +}) + +test('print appropriate message for a single package', (t) => { + t.deepEqual( + getPrintFundingReport({ + fund: true, + idealTree: { + name: 'foo', + version: '1.0.0', + children: [ + { + package: { + name: 'bar', + version: '1.0.0', + funding: { type: 'foo', url: 'http://example.com' } + } + } + ] + } + }), + `${EOL}1 package is looking for funding.${EOL}Run "npm fund" to find out more.`, + 'should print single package message' + ) + t.end() +}) + +test('print appropriate message for many packages', (t) => { + t.deepEqual( + getPrintFundingReport({ + fund: true, + idealTree: { + name: 'foo', + version: '1.0.0', + children: [ + { + package: { + name: 'bar', + version: '1.0.0', + funding: { type: 'foo', url: 'http://example.com' } + } + }, + { + package: { + name: 'lorem', + version: '1.0.0', + funding: { type: 'foo', url: 'http://example.com' } + } + }, + { + package: { + name: 'ipsum', + version: '1.0.0', + funding: { type: 'foo', url: 'http://example.com' } + } + } + ] + } + }), + `${EOL}3 packages are looking for funding.${EOL}Run "npm fund" to find out more.`, + 'should print many package message' + ) + t.end() +}) diff --git a/test/tap/repo.js b/test/tap/repo.js index 0ee50af192cb1..3e97fdeaed228 100644 --- a/test/tap/repo.js +++ b/test/tap/repo.js @@ -41,6 +41,41 @@ test('npm repo underscore', function (t) { }) }) +test('npm repo underscore --json', function (t) { + mr({ port: common.port }, function (er, s) { + common.npm([ + 'repo', 'underscore', + '--json', + '--registry=' + common.registry, + '--loglevel=silent', + '--no-browser' + ], opts, function (err, code, stdout, stderr) { + t.ifError(err, 'repo command ran without error') + t.equal(code, 0, 'exit ok') + t.matchSnapshot(stdout, 'should print json result') + s.close() + t.end() + }) + }) +}) + +test('npm repo underscore --no-browser', function (t) { + mr({ port: common.port }, function (er, s) { + common.npm([ + 'repo', 'underscore', + '--no-browser', + '--registry=' + common.registry, + '--loglevel=silent' + ], opts, function (err, code, stdout, stderr) { + t.ifError(err, 'repo command ran without error') + t.equal(code, 0, 'exit ok') + t.matchSnapshot(stdout, 'should print alternative msg') + s.close() + t.end() + }) + }) +}) + test('npm repo optimist - github (https://)', function (t) { mr({ port: common.port }, function (er, s) { common.npm([ diff --git a/test/tap/support.js b/test/tap/support.js deleted file mode 100644 index 93d4887423a13..0000000000000 --- a/test/tap/support.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict' -var test = require('tap').test -var Tacks = require('tacks') -var path = require('path') -var Dir = Tacks.Dir -var File = Tacks.File -var common = require('../common-tap.js') - -var fixturepath = common.pkg -var fixture = new Tacks(Dir({ - 'package.json': File({ - name: 'a', - version: '0.0.0', - dependencies: { 'hassupport': '7.7.7' } - }), - 'node_modules': Dir({ - hassupport: Dir({ - 'package.json': File({ - name: 'hassupport', - version: '7.7.7', - homepage: 'http://example.com/project', - support: 'http://example.com/project/donate' - }) - }) - }) -})) - -test('setup', function (t) { - fixture.remove(fixturepath) - fixture.create(fixturepath) - t.end() -}) - -test('support --json', function (t) { - common.npm(['support', '--json'], {cwd: fixturepath}, function (err, code, stdout, stderr) { - if (err) throw err - t.is(code, 0, 'exited 0') - t.is(stderr, '', 'no warnings') - var parsed - t.doesNotThrow(function () { - parsed = JSON.parse(stdout) - }, 'valid JSON') - t.deepEqual( - parsed, - [ - { - name: 'hassupport', - version: '7.7.7', - homepage: 'http://example.com/project', - support: 'http://example.com/project/donate', - path: path.resolve(fixturepath, 'node_modules', 'hassupport') - } - ], - 'output data' - ) - t.end() - }) -}) - -test('support', function (t) { - common.npm(['support'], {cwd: fixturepath}, function (err, code, stdout, stderr) { - if (err) throw err - t.is(code, 0, 'exited 0') - t.is(stderr, '', 'no warnings') - t.includes(stdout, 'hassupport', 'outputs project name') - t.includes(stdout, '7.7.7', 'outputs project version') - t.includes(stdout, 'http://example.com/project', 'outputs contributor homepage') - t.includes(stdout, 'http://example.com/project/donate', 'outputs support link') - t.end() - }) -}) - -test('cleanup', function (t) { - t.pass(fixturepath) - fixture.remove(fixturepath) - t.end() -}) diff --git a/test/tap/utils.funding.js b/test/tap/utils.funding.js new file mode 100644 index 0000000000000..60a7a1e67eb5a --- /dev/null +++ b/test/tap/utils.funding.js @@ -0,0 +1,480 @@ +'use strict' + +const { test } = require('tap') +const { getFundingInfo } = require('../../lib/utils/funding') + +test('empty tree', (t) => { + t.deepEqual( + getFundingInfo({}), + { + name: null, + dependencies: {}, + length: 0 + }, + 'should return empty list' + ) + t.end() +}) + +test('single item missing funding', (t) => { + t.deepEqual( + getFundingInfo({ name: 'project', + dependencies: { + 'single-item': { + name: 'single-item', + version: '1.0.0' + } + }}), + { + name: 'project', + dependencies: {}, + length: 0 + }, + 'should return empty list' + ) + t.end() +}) + +test('use path if name is missing', (t) => { + t.deepEqual( + getFundingInfo({ name: undefined, + path: '/tmp/foo', + children: { + 'single-item': { + name: 'single-item', + version: '1.0.0' + } + }}), + { + name: '/tmp/foo', + dependencies: {}, + length: 0 + }, + 'should use path as top level name' + ) + t.end() +}) + +test('single item tree', (t) => { + t.deepEqual( + getFundingInfo({ name: 'project', + dependencies: { + 'single-item': { + name: 'single-item', + version: '1.0.0', + funding: { + type: 'foo', + url: 'http://example.com' + } + } + }}), + { + name: 'project', + dependencies: { + 'single-item': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'http://example.com' + } + } + }, + length: 1 + }, + 'should return list with a single item' + ) + t.end() +}) + +test('duplicate items along the tree', (t) => { + t.deepEqual( + getFundingInfo({ name: 'project', + version: '2.3.4', + dependencies: { + 'single-item': { + name: 'single-item', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'shared-top-first': { + name: 'shared-top-first', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + }, + 'sub-dep': { + name: 'sub-dep', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'shared-nested-first': { + name: 'shared-nested-first', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'shared-top-first': { + name: 'shared-top-first', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + } + } + } + } + }, + 'shared-nested-first': { + name: 'shared-nested-first', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + } + } + } + }}), + { + name: 'project', + version: '2.3.4', + dependencies: { + 'single-item': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'shared-top-first': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + }, + 'sub-dep': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + }, + 'shared-nested-first': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + } + } + } + }, + length: 4 + }, + 'should return list with a single item' + ) + t.end() +}) + +test('multi-level nested items tree', (t) => { + t.deepEqual( + getFundingInfo({ name: 'project', + dependencies: { + 'first-level-dep': { + name: 'first-level-dep', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'sub-dep': { + name: 'sub-dep', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + package: { + name: 'sub-sub-dep', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: {} + } + } + } + } + } + }}), + { + name: 'project', + dependencies: { + 'first-level-dep': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'sub-dep': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'sub-sub-dep': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + } + } + } + } + } + }, + length: 3 + }, + 'should return list with all items' + ) + t.end() +}) + +test('missing fund nested items tree', (t) => { + t.deepEqual( + getFundingInfo({ name: 'project', + dependencies: { + 'first-level-dep': { + name: 'first-level-dep', + version: '1.0.0', + funding: { + type: 'foo' + }, + dependencies: { + 'sub-dep': { + name: 'sub-dep', + version: '1.0.0', + dependencies: { + 'sub-sub-dep-01': { + name: 'sub-sub-dep-01', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'non-funding-child': { + name: 'non-funding-child', + version: '1.0.0', + dependencies: { + 'sub-sub-sub-dep': { + name: 'sub-sub-sub-dep', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + } + } + } + } + }, + 'sub-sub-dep-02': { + name: 'sub-sub-dep-02', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: {} + }, + 'sub-sub-dep-03': { + name: 'sub-sub-dep-03', + version: '1.0.0', + funding: { + type: 'foo', + url: 'git://example.git' + }, + dependencies: { + 'sub-sub-sub-dep-03': { + name: 'sub-sub-sub-dep-03', + version: '1.0.0', + dependencies: { + 'sub-sub-sub-sub-dep': { + name: 'sub-sub-sub-sub-dep', + version: '1.0.0', + funding: { + type: 'foo', + url: 'http://example.com' + } + } + } + } + } + } + } + } + } + } + }}), + { + name: 'project', + dependencies: { + 'sub-sub-dep-01': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'sub-sub-sub-dep': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + } + } + }, + 'sub-sub-dep-02': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + }, + 'sub-sub-sub-sub-dep': { + version: '1.0.0', + funding: { + type: 'foo', + url: 'http://example.com' + } + } + }, + length: 4 + }, + 'should return list excluding missing funding items' + ) + t.end() +}) + +test('countOnly option', (t) => { + t.deepEqual( + getFundingInfo({ name: 'project', + dependencies: { + 'first-level-dep': { + name: 'first-level-dep', + version: '1.0.0', + funding: { + type: 'foo' + }, + dependencies: { + 'sub-dep': { + name: 'sub-dep', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'sub-sub-dep': { + name: 'sub-sub-dep', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + }, + dependencies: {} + } + }, + 'sub-sub-dep': { + name: 'sub-sub-dep', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + } + } + } + }}, { countOnly: true }), + { + length: 2 + }, + 'should return only the length property' + ) + t.end() +}) + +test('handle different versions', (t) => { + t.deepEqual( + getFundingInfo({ name: 'project', + dependencies: { + foo: { + name: 'foo', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + bar: { + name: 'bar', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + } + } + }, + lorem: { + dependencies: { + fooo: { + name: 'foo', + version: '2.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + }, + dependencies: { + 'foo-bar': { + name: 'foo-bar', + version: '1.0.0', + funding: { + type: 'foo', + url: 'https://example.com' + } + } + } + } + } + } + } + }, { countOnly: true }), + { + length: 4 + }, + 'should treat different versions as diff packages' + ) + t.end() +})