Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
26 changes: 23 additions & 3 deletions lib/commands/outdated.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,23 @@ class Outdated extends ArboristWorkspaceCmd {
}

async #getOutdatedInfo (edge) {
const alias = safeNpa(edge.spec)?.subSpec
// Global packages are iterated as Nodes (no parent declares them as deps),
// so `edge.spec` is undefined. Detect alias installs (e.g.
// `npm install -g cat@npm:dog@1.0.1`) by comparing the directory name
// (`node.name`) to the package's own name (`node.packageName`), and
// synthesize an alias spec so the registry lookup below targets the
// aliased package instead of the wrapper directory name.
// See https://github.com/npm/cli/issues/9706
const isGlobalAliasNode = !edge.spec
&& edge.packageName
&& edge.name !== edge.packageName
&& edge.version
const synthesizedSpec = isGlobalAliasNode
? `npm:${edge.packageName}@${edge.version}`
: null
const effectiveEdgeSpec = synthesizedSpec ?? edge.spec

const alias = safeNpa(effectiveEdgeSpec)?.subSpec
const spec = npa(alias ? alias.name : edge.name)
const node = edge.to || edge
const { path, location, package: { version: current } = {} } = node
Expand All @@ -178,7 +194,7 @@ class Outdated extends ArboristWorkspaceCmd {
}

// if it's not a range, version, or tag, skip it
if (!safeNpa(`${edge.name}@${edge.spec}`)?.registry) {
if (!safeNpa(`${edge.name}@${effectiveEdgeSpec}`)?.registry) {
return null
}

Expand All @@ -195,7 +211,11 @@ class Outdated extends ArboristWorkspaceCmd {
const latest = pickManifest(packument, '*', pickOpts)
if (!current || current !== wanted.version || wanted.version !== latest.version) {
this.#list.push({
name: alias ? edge.spec.replace('npm', edge.name) : edge.name,
name: alias
? (synthesizedSpec
? `${edge.name}@${synthesizedSpec}`
: edge.spec.replace('npm', edge.name))
: edge.name,
path,
type,
current,
Expand Down
56 changes: 56 additions & 0 deletions test/lib/commands/outdated.js
Original file line number Diff line number Diff line change
Expand Up @@ -803,3 +803,59 @@ t.test('min-release-age-exclude', async t => {
t.match(joinedOutput(), '2.0.0', 'glob-excluded package shows newer version')
})
})

t.test('global alias packages report alias spec, not aliased package version', async t => {
// Regression test for https://github.com/npm/cli/issues/9706
// When a package is installed globally via an alias spec
// (e.g. `npm install -g cowsay@npm:dog@1.0.1`), `npm outdated -g` was
// resolving the wrapper directory name against the registry instead of
// the aliased package, so the `Current` column showed the aliased
// package version while `Wanted`/`Latest` came from a totally
// different packument.
const globalAliasFixture = {
node_modules: {
cat: {
'package.json': JSON.stringify({
name: 'dog',
version: '1.0.1',
_from: 'cat@npm:dog@1.0.1',
_requested: {
type: 'alias',
name: 'cat',
escapedName: 'cat',
rawSpec: 'npm:dog@1.0.1',
saveSpec: 'npm:dog@1.0.1',
fetchSpec: 'latest',
subSpec: {
type: 'version',
registry: true,
name: 'dog',
escapedName: 'dog',
rawSpec: '1.0.1',
saveSpec: null,
fetchSpec: '1.0.1',
},
},
}, null, 2),
},
},
}

const { outdated, joinedOutput } = await mockNpm(t, {
globalPrefixDir: globalAliasFixture,
config: { global: true },
})
await outdated.exec([])

const out = joinedOutput()
// The row must describe the alias (`cat@npm:dog`) so users can tell
// which package on disk is being reported, and the Latest column must
// come from the aliased package's packument (`dog`@2.0.0), not from
// the wrapper name (`cat` would resolve to its own 1.0.1 latest).
t.match(out, /cat@npm:dog/,
'output identifies the row by its alias spec')
t.match(out, /npm:dog@1\.0\.1/,
'Current column reflects the aliased package version with alias prefix')
t.match(out, /\b2\.0\.0\b/,
'Latest column comes from the aliased package\'s packument (dog@2.0.0)')
})