Skip to content

fix(ls): correctly show deduped dependencies #8217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: latest
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 127 additions & 117 deletions lib/commands/ls.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason workspacesEnabled is pulling from flatoptions is because it's derived from workspaces config. includeWorkspaceRoot has no such distinction so we should pull it from config like the rest of this command does.

We know this is less than ideal, but fixing this disparity is a future concern.


const path = global ? resolve(this.npm.globalDir, '..') : this.npm.prefix

Expand All @@ -84,95 +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 seenNodes = new Map()
const problems = new Set()

const result = exploreDependencyGraph({
node: tree,
getChildren (node, nodeResult) {
const seenPaths = new Set()
const workspace = node.isWorkspace
const currentDepth = workspace ? 0 : node[_depth]
const target = (node.target)?.edgesOut

const shouldSkipChildren =
(currentDepth > depthToPrint) || !nodeResult

return (shouldSkipChildren || !target)
? []
: [...target.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 (node) {
// 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
wsNodes,
configs: {
json,
parseable,
depthToPrint,
workspacesEnabled,
link,
omit,
includeWorkspaceRoot,
args,
chalk,
global,
long,
},
opts: { json, parseable },
seenNodes,
problems,
})

// handle the special case of a broken package.json in the root folder
Expand Down Expand Up @@ -227,51 +161,149 @@ 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,
getChildren,
visit,
opts,
wsNodes,
configs,
seenNodes,
problems,
cache = new Map(),
traversePathMap = new Map(),
}) => {
const { json, parseable } = opts
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)
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
const traversePath = [...(traversePathMap.get(currentNodeResult[_parent]) || [])]
const isCircular = traversePath?.includes(node.pkgid)
traversePath.push(node.pkgid)
traversePathMap.set(currentNodeResult, traversePath)
// 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
const children = isCircular || shouldSkipChildren || !currentNodeResult
? []
: getChildren(node, 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,
getChildren,
visit,
opts,
wsNodes,
configs,
seenNodes,
problems,
cache,
traversePathMap,
})
Expand Down Expand Up @@ -503,28 +535,6 @@ const filterByPositionalArgs = (args, { node }) =>
(spec) => (node.satisfies && node.satisfies(spec))
) : true

const augmentNodesWithMetadata = ({
args,
currentDepth,
nodeResult,
}) => (node) => {
// _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
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 })
// _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, unicode }) => {
Expand Down
Loading