diff --git a/lib/commands/ls.js b/lib/commands/ls.js index bc8beb007e809..7fac0c141e408 100644 --- a/lib/commands/ls.js +++ b/lib/commands/ls.js @@ -1,6 +1,5 @@ const { resolve, relative, sep } = require('node:path') const archy = require('archy') -const { breadth } = require('treeverse') const npa = require('npm-package-arg') const { output } = require('proc-log') const ArboristWorkspaceCmd = require('../arborist-cmd.js') @@ -59,9 +58,16 @@ class LS extends ArboristWorkspaceCmd { const unicode = this.npm.config.get('unicode') const packageLockOnly = this.npm.config.get('package-lock-only') const workspacesEnabled = this.npm.flatOptions.workspacesEnabled + const includeWorkspaceRoot = this.npm.flatOptions.includeWorkspaceRoot const path = global ? resolve(this.npm.globalDir, '..') : this.npm.prefix + // defines special handling of printed depth when filtering with args + const filterDefaultDepth = depth === null ? Infinity : depth + const depthToPrint = (all || args.length) + ? filterDefaultDepth + : (depth || 0) + const Arborist = require('@npmcli/arborist') const arb = new Arborist({ @@ -79,98 +85,28 @@ class LS extends ArboristWorkspaceCmd { if (this.workspaceNames && this.workspaceNames.length) { wsNodes = arb.workspaceNodes(tree, this.workspaceNames) } - const filterBySelectedWorkspaces = edge => { - if (!workspacesEnabled - && edge.from.isProjectRoot - && edge.to.isWorkspace - ) { - return false - } - - if (!wsNodes || !wsNodes.length) { - return true - } - - if (this.npm.flatOptions.includeWorkspaceRoot - && edge.to && !edge.to.isWorkspace) { - return true - } - - if (edge.from.isProjectRoot) { - return (edge.to - && edge.to.isWorkspace - && wsNodes.includes(edge.to.target)) - } - - return true - } - const seenItems = new Set() const seenNodes = new Map() const problems = new Set() - // defines special handling of printed depth when filtering with args - const filterDefaultDepth = depth === null ? Infinity : depth - const depthToPrint = (all || args.length) - ? filterDefaultDepth - : (depth || 0) - - // add root node of tree to list of seenNodes - seenNodes.set(tree.path, tree) - - // tree traversal happens here, using treeverse.breadth - const result = await breadth({ - tree, - // recursive method, `node` is going to be the current elem (starting from - // the `tree` obj) that was just visited in the `visit` method below - // `nodeResult` is going to be the returned `item` from `visit` - getChildren (node, nodeResult) { - const seenPaths = new Set() - const workspace = node.isWorkspace - const currentDepth = workspace ? 0 : node[_depth] - const shouldSkipChildren = - !(node instanceof Arborist.Node) || (currentDepth > depthToPrint) - return (shouldSkipChildren) - ? [] - : [...(node.target).edgesOut.values()] - .filter(filterBySelectedWorkspaces) - .filter(currentDepth === 0 ? filterByEdgesTypes({ - link, - omit, - }) : () => true) - .map(mapEdgesToNodes({ seenPaths })) - .concat(appendExtraneousChildren({ node, seenPaths })) - .sort(sortAlphabetically) - .map(augmentNodesWithMetadata({ - args, - currentDepth, - nodeResult, - seenNodes, - })) - }, - // visit each `node` of the `tree`, returning an `item` - these are - // the elements that will be used to build the final output - visit (node) { - node[_problems] = getProblems(node, { global }) - - const item = json - ? getJsonOutputItem(node, { global, long }) - : parseable - ? null - : getHumanOutputItem(node, { args, chalk, global, long }) - - // loop through list of node problems to add them to global list - if (node[_include]) { - for (const problem of node[_problems]) { - problems.add(problem) - } - } - - seenItems.add(item) - - // return a promise so we don't blow the stack - return Promise.resolve(item) + const result = exploreDependencyGraph({ + node: tree, + wsNodes, + configs: { + json, + parseable, + depthToPrint, + workspacesEnabled, + link, + omit, + includeWorkspaceRoot, + args, + chalk, + global, + long, }, + seenNodes, + problems, }) // handle the special case of a broken package.json in the root folder @@ -178,11 +114,11 @@ class LS extends ArboristWorkspaceCmd { e.code === 'EJSONPARSE' && e.path === resolve(path, 'package.json')) if (json) { - output.buffer(jsonOutput({ path, problems, result, rootError, seenItems })) + output.buffer(jsonOutput({ path, problems, result, rootError })) } else { output.standard(parseable ? parseableOutput({ seenNodes, global, long }) - : humanOutput({ chalk, result, seenItems, unicode }) + : humanOutput({ chalk, result, unicode }) ) } @@ -225,6 +161,168 @@ class LS extends ArboristWorkspaceCmd { module.exports = LS +const createWsFilter = (wsNodes, options) => edge => { + const { workspacesEnabled, includeWorkspaceRoot } = options + + if (!workspacesEnabled + && edge.from.isProjectRoot + && edge.to.isWorkspace + ) { + return false + } + + if (!wsNodes || !wsNodes.length) { + return true + } + + if (includeWorkspaceRoot + && edge.to && !edge.to.isWorkspace) { + return true + } + + if (edge.from.isProjectRoot) { + return (edge.to + && edge.to.isWorkspace + && wsNodes.includes(edge.to.target)) + } + + return true +} + +const visit = (node, seenNodes, problems, opts) => { + const { json, parseable, args, chalk, global, long } = opts + // add to seenNodes as soon as we visit and not when the children are calculated in previous call + if (seenNodes.has(node.path)) { + node[_dedupe] = !node[_missing] + } else { + seenNodes.set(node.path, node) + } + + node[_problems] = getProblems(node, { global }) + + const item = json + ? getJsonOutputItem(node, { global, long }) + : parseable + ? { + pkgid: node.pkgid, + path: node.path, + [_dedupe]: node[_dedupe], + [_parent]: node[_parent], + } + : getHumanOutputItem(node, { args, chalk, global, long }) + + // loop through list of node problems to add them to global list + if (node[_include]) { + for (const problem of node[_problems]) { + problems.add(problem) + } + } + return item +} + +const getChildren = (node, wsNodes, options) => { + const { link, omit } = options + const seenPaths = new Set() + const workspace = node.isWorkspace + const currentDepth = workspace ? 0 : node[_depth] + const target = (node.target)?.edgesOut + if (!target) { + return [] + } + return [...target.values()] + .filter(createWsFilter(wsNodes, options)) + .filter(currentDepth === 0 ? filterByEdgesTypes({ + link, + omit, + }) : () => true) + .map(mapEdgesToNodes({ seenPaths })) + .concat(appendExtraneousChildren({ node, seenPaths })) + .sort(sortAlphabetically) +} + +const exploreDependencyGraph = ({ + node, + wsNodes, + configs, + seenNodes, + problems, + cache = new Map(), + traversePathMap = new Map(), +}) => { + const { json, parseable, depthToPrint, args } = configs + + // cahce is for already visited nodes results + // if the node is already seen, we can return it from cache + if (cache.has(node.path)) { + return cache.get(node.path) + } + + const currentNodeResult = visit(node, seenNodes, problems, configs) + + // how the this node is explored + // so if the explored path contains this node again then it's a cycle + // and we don't want to explore it again + // Track the path of pkgids to detect cycles efficiently + const parentTraversePath = traversePathMap.get(currentNodeResult[_parent]) || [] + const isCircular = parentTraversePath.includes(node.pkgid) + const currentPath = [...parentTraversePath, node.pkgid] + traversePathMap.set(currentNodeResult, currentPath) + + // we want to start using cache after node is identified as a deduped + if (node[_dedupe]) { + cache.set(node.path, currentNodeResult) + } + + const currentDepth = node.isWorkspace ? 0 : node[_depth] + const shouldSkipChildren = + (currentDepth > depthToPrint) + + // Get children of current node + const children = isCircular || shouldSkipChildren || !currentNodeResult + ? [] + : getChildren(node, wsNodes, configs) + + // Recurse on each child node + for (const child of children) { + // _parent is going to be a ref to a traversed node (returned from + // getHumanOutputItem, getJsonOutputItem, etc) so that we have an easy + // shortcut to place new nodes in their right place during tree traversal + child[_parent] = currentNodeResult + // _include is the property that allow us to filter based on position args + // e.g: `npm ls foo`, `npm ls simple-output@2` + // _filteredBy is used to apply extra color info to the item that + // was used in args in order to filter + child[_filteredBy] = child[_include] = + filterByPositionalArgs(args, { node: child }) + // _depth keeps track of how many levels deep tree traversal currently is + // so that we can `npm ls --depth=1` + child[_depth] = currentDepth + 1 + + const childResult = exploreDependencyGraph({ + node: child, + wsNodes, + configs, + seenNodes, + problems, + cache, + traversePathMap, + }) + // include current node if any of its children are included + currentNodeResult[_include] = currentNodeResult[_include] || childResult[_include] + + if (childResult[_include] && !parseable) { + if (json) { + currentNodeResult.dependencies = currentNodeResult.dependencies || {} + currentNodeResult.dependencies[childResult[_name]] = childResult + } else { + currentNodeResult.nodes.push(childResult) + } + } + } + + return currentNodeResult +} + const isGitNode = (node) => { if (!node.resolved) { return @@ -262,26 +360,6 @@ const getProblems = (node, { global }) => { return problems } -// annotates _parent and _include metadata into the resulting -// item obj allowing for filtering out results during output -const augmentItemWithIncludeMetadata = (node, item) => { - item[_parent] = node[_parent] - item[_include] = node[_include] - - // append current item to its parent.nodes which is the - // structure expected by archy in order to print tree - if (node[_include]) { - // includes all ancestors of included node - let p = node[_parent] - while (p) { - p[_include] = true - p = p[_parent] - } - } - - return item -} - const getHumanOutputItem = (node, { args, chalk, global, long }) => { const { pkgid, path } = node const workspacePkgId = chalk.blueBright(pkgid) @@ -339,13 +417,13 @@ const getHumanOutputItem = (node, { args, chalk, global, long }) => { ) + (isGitNode(node) ? ` (${node.resolved})` : '') + (node.isLink ? ` -> ${relativePrefix}${targetLocation}` : '') + - (long ? `\n${node.package.description || ''}` : '') + (long ? `\n${node.package?.description || ''}` : '') - return augmentItemWithIncludeMetadata(node, { label, nodes: [] }) + return ({ label, nodes: [], [_include]: node[_include], [_parent]: node[_parent] }) } const getJsonOutputItem = (node, { global, long }) => { - const item = {} + const item = { [_include]: node[_include], [_parent]: node[_parent] } if (node.version) { item.version = node.version @@ -402,7 +480,7 @@ const getJsonOutputItem = (node, { global, long }) => { item.problems = [...node[_problems]] } - return augmentItemWithIncludeMetadata(node, item) + return item } const filterByEdgesTypes = ({ link, omit }) => (edge) => { @@ -457,69 +535,9 @@ const filterByPositionalArgs = (args, { node }) => (spec) => (node.satisfies && node.satisfies(spec)) ) : true -const augmentNodesWithMetadata = ({ - args, - currentDepth, - nodeResult, - seenNodes, -}) => (node) => { - // if the original edge was a deduped dep, treeverse will fail to - // revisit that node in tree traversal logic, so we make it so that - // we have a diff obj for deduped nodes: - if (seenNodes.has(node.path)) { - const { realpath, root } = node - const targetLocation = root ? relative(root.realpath, realpath) - : node.targetLocation - node = { - name: node.name, - version: node.version, - pkgid: node.pkgid, - package: node.package, - path: node.path, - isLink: node.isLink, - realpath: node.realpath, - targetLocation, - [_type]: node[_type], - [_invalid]: node[_invalid], - [_missing]: node[_missing], - // if it's missing, it's not deduped, it's just missing - [_dedupe]: !node[_missing], - } - } else { - // keeps track of already seen nodes in order to check for dedupes - seenNodes.set(node.path, node) - } - - // _parent is going to be a ref to a treeverse-visited node (returned from - // getHumanOutputItem, getJsonOutputItem, etc) so that we have an easy - // shortcut to place new nodes in their right place during tree traversal - node[_parent] = nodeResult - // _include is the property that allow us to filter based on position args - // e.g: `npm ls foo`, `npm ls simple-output@2` - // _filteredBy is used to apply extra color info to the item that - // was used in args in order to filter - node[_filteredBy] = node[_include] = - filterByPositionalArgs(args, { node: seenNodes.get(node.path) }) - // _depth keeps track of how many levels deep tree traversal currently is - // so that we can `npm ls --depth=1` - node[_depth] = currentDepth + 1 - - return node -} - const sortAlphabetically = ({ pkgid: a }, { pkgid: b }) => localeCompare(a, b) -const humanOutput = ({ chalk, result, seenItems, unicode }) => { - // we need to traverse the entire tree in order to determine which items - // should be included (since a nested transitive included dep will make it - // so that all its ancestors should be displayed) - // here is where we put items in their expected place for archy output - for (const item of seenItems) { - if (item[_include] && item[_parent]) { - item[_parent].nodes.push(item) - } - } - +const humanOutput = ({ chalk, result, unicode }) => { if (!result.nodes.length) { result.nodes = ['(empty)'] } @@ -528,7 +546,7 @@ const humanOutput = ({ chalk, result, seenItems, unicode }) => { return chalk.reset(archyOutput) } -const jsonOutput = ({ path, problems, result, rootError, seenItems }) => { +const jsonOutput = ({ path, problems, result, rootError }) => { if (problems.size) { result.problems = [...problems] } @@ -541,22 +559,6 @@ const jsonOutput = ({ path, problems, result, rootError, seenItems }) => { result.invalid = true } - // we need to traverse the entire tree in order to determine which items - // should be included (since a nested transitive included dep will make it - // so that all its ancestors should be displayed) - // here is where we put items in their expected place for json output - for (const item of seenItems) { - // append current item to its parent item.dependencies obj in order - // to provide a json object structure that represents the installed tree - if (item[_include] && item[_parent]) { - if (!item[_parent].dependencies) { - item[_parent].dependencies = {} - } - - item[_parent].dependencies[item[_name]] = item - } - } - return result } diff --git a/tap-snapshots/test/lib/commands/ls.js.test.cjs b/tap-snapshots/test/lib/commands/ls.js.test.cjs index 86394b702f19a..631b7a6c4cd35 100644 --- a/tap-snapshots/test/lib/commands/ls.js.test.cjs +++ b/tap-snapshots/test/lib/commands/ls.js.test.cjs @@ -145,12 +145,12 @@ exports[`test/lib/commands/ls.js TAP ls --parseable --long > should output tree {CWD}/prefix:test-npm-ls@1.0.0 {CWD}/prefix/node_modules/chai:chai@1.0.0 {CWD}/prefix/node_modules/dev-dep:dev-dep@1.0.0 +{CWD}/prefix/node_modules/foo:foo@1.0.0 +{CWD}/prefix/node_modules/dog:dog@1.0.0 {CWD}/prefix/node_modules/optional-dep:optional-dep@1.0.0 {CWD}/prefix/node_modules/peer-dep:peer-dep@1.0.0 {CWD}/prefix/node_modules/prod-dep:prod-dep@1.0.0 -{CWD}/prefix/node_modules/foo:foo@1.0.0 {CWD}/prefix/node_modules/prod-dep/node_modules/dog:dog@2.0.0 -{CWD}/prefix/node_modules/dog:dog@1.0.0 ` exports[`test/lib/commands/ls.js TAP ls --parseable --long missing/invalid/extraneous > should output parseable result containing EXTRANEOUS/INVALID labels 1`] = ` @@ -164,13 +164,13 @@ exports[`test/lib/commands/ls.js TAP ls --parseable --long print symlink target {CWD}/prefix:test-npm-ls@1.0.0 {CWD}/prefix/node_modules/chai:chai@1.0.0 {CWD}/prefix/node_modules/dev-dep:dev-dep@1.0.0 +{CWD}/prefix/node_modules/foo:foo@1.0.0 +{CWD}/prefix/node_modules/dog:dog@1.0.0 {CWD}/prefix/node_modules/linked-dep:linked-dep@1.0.0:{CWD}/prefix/linked-dep {CWD}/prefix/node_modules/optional-dep:optional-dep@1.0.0 {CWD}/prefix/node_modules/peer-dep:peer-dep@1.0.0 {CWD}/prefix/node_modules/prod-dep:prod-dep@1.0.0 -{CWD}/prefix/node_modules/foo:foo@1.0.0 {CWD}/prefix/node_modules/prod-dep/node_modules/dog:dog@2.0.0 -{CWD}/prefix/node_modules/dog:dog@1.0.0 ` exports[`test/lib/commands/ls.js TAP ls --parseable --long with extraneous deps > should output long parseable output with extraneous info 1`] = ` @@ -263,24 +263,24 @@ exports[`test/lib/commands/ls.js TAP ls --parseable unmet optional dep > should {CWD}/prefix {CWD}/prefix/node_modules/chai {CWD}/prefix/node_modules/dev-dep +{CWD}/prefix/node_modules/foo +{CWD}/prefix/node_modules/dog {CWD}/prefix/node_modules/optional-dep {CWD}/prefix/node_modules/peer-dep {CWD}/prefix/node_modules/prod-dep -{CWD}/prefix/node_modules/foo {CWD}/prefix/node_modules/prod-dep/node_modules/dog -{CWD}/prefix/node_modules/dog ` exports[`test/lib/commands/ls.js TAP ls --parseable unmet peer dep > should output parseable signaling missing peer dep in problems 1`] = ` {CWD}/prefix {CWD}/prefix/node_modules/chai {CWD}/prefix/node_modules/dev-dep +{CWD}/prefix/node_modules/foo +{CWD}/prefix/node_modules/dog {CWD}/prefix/node_modules/optional-dep {CWD}/prefix/node_modules/peer-dep {CWD}/prefix/node_modules/prod-dep -{CWD}/prefix/node_modules/foo {CWD}/prefix/node_modules/prod-dep/node_modules/dog -{CWD}/prefix/node_modules/dog ` exports[`test/lib/commands/ls.js TAP ls --parseable using aliases > should output tree containing aliases 1`] = ` @@ -387,8 +387,8 @@ test-pkg-arg-filter-with-depth-opt@1.0.0 {CWD}/prefix exports[`test/lib/commands/ls.js TAP ls filtering by child of missing dep > should print tree and not duplicate child of missing items 1`] = ` filter-by-child-of-missing-dep@1.0.0 {CWD}/prefix +-- b@1.0.0 extraneous -| \`-- c@1.0.0 deduped -+-- c@1.0.0 extraneous +| \`-- c@1.0.0 extraneous ++-- c@1.0.0 deduped extraneous \`-- d@1.0.0 extraneous \`-- c@2.0.0 extraneous ` @@ -408,8 +408,8 @@ exports[`test/lib/commands/ls.js TAP ls global > should print tree and not mark exports[`test/lib/commands/ls.js TAP ls invalid deduped dep > should output tree signaling mismatching peer dep in problems 1`] = ` invalid-deduped-dep@1.0.0 {CWD}/prefix +-- a@1.0.0 -| \`-- b@1.0.0 deduped invalid: "^2.0.0" from the root project, "^2.0.0" from node_modules/a -\`-- b@1.0.0 invalid: "^2.0.0" from the root project, "^2.0.0" from node_modules/a +| \`-- b@1.0.0 invalid: "^2.0.0" from the root project, "^2.0.0" from node_modules/a +\`-- b@1.0.0 deduped invalid: "^2.0.0" from the root project, "^2.0.0" from node_modules/a  ` @@ -439,8 +439,8 @@ workspaces-tree@1.0.0 {CWD}/prefix exports[`test/lib/commands/ls.js TAP ls loading a tree containing workspaces should filter single workspace > output 1`] = ` workspaces-tree@1.0.0 {CWD}/prefix +-- a@1.0.0 -> ./a -| \`-- d@1.0.0 deduped -> ./d -\`-- d@1.0.0 -> ./d +| \`-- d@1.0.0 -> ./d +\`-- d@1.0.0 deduped -> ./d ` exports[`test/lib/commands/ls.js TAP ls loading a tree containing workspaces should filter using workspace config > output 1`] = ` @@ -466,11 +466,13 @@ workspaces-tree@1.0.0 {CWD}/prefix +-- a@1.0.0 -> ./a | +-- baz@1.0.0 | +-- c@1.0.0 -| \`-- d@1.0.0 deduped -> ./d +| \`-- d@1.0.0 -> ./d +| \`-- foo@1.1.1 +| \`-- bar@1.0.0 +-- b@1.0.0 -> ./b -+-- d@1.0.0 -> ./d -| \`-- foo@1.1.1 -| \`-- bar@1.0.0 ++-- d@1.0.0 deduped -> ./d +| \`-- foo@1.1.1 deduped +| \`-- bar@1.0.0 deduped +-- e@1.0.0 -> ./group/e +-- f@1.0.0 -> ./group/f \`-- pacote@1.0.0 @@ -480,11 +482,13 @@ exports[`test/lib/commands/ls.js TAP ls loading a tree containing workspaces sho workspaces-tree@1.0.0 {CWD}/prefix +-- a@1.0.0 -> ./a | +-- c@1.0.0 -| \`-- d@1.0.0 deduped -> ./d +| \`-- d@1.0.0 -> ./d +| \`-- foo@1.1.1 +| \`-- bar@1.0.0 +-- b@1.0.0 -> ./b -+-- d@1.0.0 -> ./d -| \`-- foo@1.1.1 -| \`-- bar@1.0.0 ++-- d@1.0.0 deduped -> ./d +| \`-- foo@1.1.1 deduped +| \`-- bar@1.0.0 deduped +-- e@1.0.0 -> ./group/e +-- f@1.0.0 -> ./group/f \`-- pacote@1.0.0 @@ -495,10 +499,11 @@ exports[`test/lib/commands/ls.js TAP ls loading a tree containing workspaces sho +-- a@1.0.0 -> ./a | +-- baz@1.0.0 | +-- c@1.0.0 -| \`-- d@1.0.0 deduped -> ./d +| \`-- d@1.0.0 -> ./d +| \`-- foo@1.1.1 +-- b@1.0.0 -> ./b -+-- d@1.0.0 -> ./d -| \`-- foo@1.1.1 ++-- d@1.0.0 deduped -> ./d +| \`-- foo@1.1.1 deduped +-- e@1.0.0 -> ./group/e +-- f@1.0.0 -> ./group/f \`-- pacote@1.0.0 @@ -523,7 +528,7 @@ exports[`test/lib/commands/ls.js TAP ls missing package.json > should output tre +-- chai@1.0.0 extraneous +-- dog@1.0.0 extraneous \`-- foo@1.0.0 extraneous - \`-- dog@1.0.0 deduped + \`-- dog@1.0.0 deduped extraneous ` exports[`test/lib/commands/ls.js TAP ls missing/invalid/extraneous > should output tree containing missing, invalid, extraneous labels 1`] = ` @@ -557,8 +562,8 @@ exports[`test/lib/commands/ls.js TAP ls overridden dep w/ color > should contain exports[`test/lib/commands/ls.js TAP ls print deduped symlinks > should output tree containing linked deps 1`] = ` print-deduped-symlinks@1.0.0 {CWD}/prefix +-- a@1.0.0 -| \`-- b@1.0.0 deduped -> ./b -\`-- b@1.0.0 -> ./b +| \`-- b@1.0.0 -> ./b +\`-- b@1.0.0 deduped -> ./b ` exports[`test/lib/commands/ls.js TAP ls resolved points to git ref > should output tree containing git refs 1`] = ` @@ -593,8 +598,8 @@ test-npm-ls@1.0.0 {CWD}/prefix exports[`test/lib/commands/ls.js TAP ls with args and dedupe entries > should print tree output containing deduped ref 1`] = ` dedupe-entries@1.0.0 {CWD}/prefix +-- @npmcli/a@1.0.0 -| \`-- @npmcli/b@1.1.2 deduped -+-- @npmcli/b@1.1.2 +| \`-- @npmcli/b@1.1.2 ++-- @npmcli/b@1.1.2 deduped \`-- @npmcli/c@1.0.0  \`-- @npmcli/b@1.1.2 deduped  @@ -603,10 +608,10 @@ exports[`test/lib/commands/ls.js TAP ls with args and dedupe entries > should pr exports[`test/lib/commands/ls.js TAP ls with args and different order of items > should print tree output containing deduped ref 1`] = ` dedupe-entries@1.0.0 {CWD}/prefix +-- @npmcli/a@1.0.0 -| \`-- @npmcli/c@1.0.0 deduped +| \`-- @npmcli/c@1.0.0 +-- @npmcli/b@1.1.2 | \`-- @npmcli/c@1.0.0 deduped -\`-- @npmcli/c@1.0.0 +\`-- @npmcli/c@1.0.0 deduped ` exports[`test/lib/commands/ls.js TAP ls with dot filter arg > should output tree contaning only occurrences of filtered by package and colored output 1`] = ` @@ -641,8 +646,8 @@ test-npm-ls@1.0.0 {CWD}/prefix exports[`test/lib/commands/ls.js TAP ls with no args dedupe entries > should print tree output containing deduped ref 1`] = ` dedupe-entries@1.0.0 {CWD}/prefix +-- @npmcli/a@1.0.0 -| \`-- @npmcli/b@1.1.2 deduped -+-- @npmcli/b@1.1.2 +| \`-- @npmcli/b@1.1.2 ++-- @npmcli/b@1.1.2 deduped \`-- @npmcli/c@1.0.0 \`-- @npmcli/b@1.1.2 deduped ` @@ -663,9 +668,11 @@ root@ {CWD}/prefix exports[`test/lib/commands/ls.js TAP show multiple invalid reasons > ls result 1`] = ` test-npm-ls@1.0.0 {CWD}/prefix +-- cat@1.0.0 invalid: "^2.0.0" from the root project -| \`-- dog@1.0.0 deduped invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat +| \`-- dog@1.0.0 invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat +| \`-- cat@1.0.0 deduped invalid: "^2.0.0" from the root project +-- chai@1.0.0 extraneous | \`-- dog@1.0.0 deduped invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai -\`-- dog@1.0.0 invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai +| \`-- cat@1.0.0 deduped invalid: "^2.0.0" from the root project +\`-- dog@1.0.0 deduped invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai \`-- cat@1.0.0 deduped invalid: "^2.0.0" from the root project ` diff --git a/test/lib/commands/ls.js b/test/lib/commands/ls.js index cf96452d6cb5d..27571d7a39235 100644 --- a/test/lib/commands/ls.js +++ b/test/lib/commands/ls.js @@ -2748,6 +2748,11 @@ t.test('ls --json', async t => { dependencies: { dog: { version: '1.0.0', + extraneous: true, + overridden: false, + problems: [ + 'extraneous: dog@1.0.0 {CWD}/prefix/node_modules/dog', + ], }, }, }, @@ -4103,6 +4108,7 @@ t.test('ls --json', async t => { dependencies: { a: { version: '1.0.0', + overridden: false, }, }, },