mirror of
https://github.com/pnpm/pnpm.git
synced 2026-01-15 02:18:31 -05:00
fix: display npm: protocol for aliased packages in list and why (#10084)
* fix: support alias resolution in pnpm why with npm: protocol * refactor: make alias required instead of optional * refactor: reorder field to put alias first
This commit is contained in:
committed by
Zoltan Kochan
parent
2194432539
commit
c206765715
9
.changeset/shaky-lines-sit.md
Normal file
9
.changeset/shaky-lines-sit.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-listing": patch
|
||||
"@pnpm/reviewing.dependencies-hierarchy": patch
|
||||
"@pnpm/types": patch
|
||||
"@pnpm/list": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
`pnpm list` and `pnpm why` now display npm: protocol for aliased packages (e.g., `foo npm:is-odd@3.0.1`) [#8660](https://github.com/pnpm/pnpm/issues/8660).
|
||||
@@ -16,6 +16,7 @@ export type IncludedDependencies = {
|
||||
export type ReadPackageHook = <Pkg extends BaseManifest> (pkg: Pkg, dir?: string) => Pkg | Promise<Pkg>
|
||||
|
||||
export interface FinderContext {
|
||||
alias: string
|
||||
name: string
|
||||
version: string
|
||||
readManifest: () => DependencyManifest
|
||||
|
||||
@@ -171,6 +171,7 @@ async function dependenciesHierarchyForPackage (
|
||||
})
|
||||
let newEntry: PackageNode | null = null
|
||||
const matchedSearched = opts.search?.({
|
||||
alias,
|
||||
name: packageInfo.name,
|
||||
version: packageInfo.version,
|
||||
readManifest,
|
||||
@@ -231,6 +232,7 @@ async function dependenciesHierarchyForPackage (
|
||||
version,
|
||||
}
|
||||
const matchedSearched = opts.search?.({
|
||||
alias: pkg.alias,
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
readManifest: () => readPackageJsonFromDirSync(pkgPath),
|
||||
|
||||
@@ -35,9 +35,10 @@ function search (
|
||||
matchName: MatchFunction
|
||||
matchVersion?: MatchFunction
|
||||
},
|
||||
{ name, version }: FinderContext
|
||||
{ alias, name, version }: FinderContext
|
||||
): boolean {
|
||||
if (!packageSelector.matchName(name)) {
|
||||
const nameMatches = packageSelector.matchName(name) || packageSelector.matchName(alias)
|
||||
if (!nameMatches) {
|
||||
return false
|
||||
}
|
||||
if (packageSelector.matchVersion == null) {
|
||||
|
||||
@@ -142,6 +142,7 @@ function getTreeHelper (
|
||||
})
|
||||
let circular: boolean
|
||||
const matchedSearched = opts.search?.({
|
||||
alias,
|
||||
name: packageInfo.name,
|
||||
version: packageInfo.version,
|
||||
readManifest,
|
||||
|
||||
@@ -37,6 +37,7 @@ test('package searcher with 2 finders', () => {
|
||||
|
||||
function mockContext (manifest: DependencyManifest) {
|
||||
return {
|
||||
alias: manifest.name,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
readManifest: () => manifest,
|
||||
|
||||
@@ -56,7 +56,21 @@ function renderParseableForPackage (
|
||||
}
|
||||
return [
|
||||
firstLine,
|
||||
...pkgs.map((pkg) => `${pkg.path}:${pkg.name}@${pkg.version}`),
|
||||
...pkgs.map((pkgNode) => {
|
||||
const node = pkgNode as PackageNode
|
||||
if (node.alias !== node.name) {
|
||||
// Only add npm: prefix if version doesn't already contain @ (to avoid file:, link:, etc.)
|
||||
if (!node.version.includes('@')) {
|
||||
return `${node.path}:${node.alias} npm:${node.name}@${node.version}`
|
||||
}
|
||||
return `${node.path}:${node.alias} ${node.version}`
|
||||
}
|
||||
// If version already contains @, it's in full format (e.g., name@file:path)
|
||||
if (node.version.includes('@')) {
|
||||
return `${node.path}:${node.version}`
|
||||
}
|
||||
return `${node.path}:${node.name}@${node.version}`
|
||||
}),
|
||||
].join('\n')
|
||||
}
|
||||
return [
|
||||
@@ -66,6 +80,7 @@ function renderParseableForPackage (
|
||||
}
|
||||
|
||||
interface PackageInfo {
|
||||
alias: string
|
||||
name: string
|
||||
version: string
|
||||
path: string
|
||||
|
||||
@@ -145,7 +145,18 @@ export async function toArchyTree (
|
||||
|
||||
function printLabel (getPkgColor: GetPkgColor, node: PackageNode): string {
|
||||
const color = getPkgColor(node)
|
||||
let txt = `${color(node.name)} ${chalk.gray(node.version)}`
|
||||
let txt: string
|
||||
if (node.alias !== node.name) {
|
||||
// When using npm: protocol alias, display as "alias npm:name@version"
|
||||
// Only add npm: prefix if version doesn't already contain @ (to avoid file:, link:, etc.)
|
||||
if (!node.version.includes('@')) {
|
||||
txt = `${color(node.alias)} ${chalk.gray(`npm:${node.name}@${node.version}`)}`
|
||||
} else {
|
||||
txt = `${color(node.alias)} ${chalk.gray(node.version)}`
|
||||
}
|
||||
} else {
|
||||
txt = `${color(node.name)} ${chalk.gray(node.version)}`
|
||||
}
|
||||
if (node.isPeer) {
|
||||
txt += ' peer'
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fixtures } from '@pnpm/test-fixtures'
|
||||
import chalk from 'chalk'
|
||||
import cliColumns from 'cli-columns'
|
||||
import { renderTree } from '../lib/renderTree.js'
|
||||
import { renderParseable } from '../lib/renderParseable.js'
|
||||
|
||||
const DEV_DEP_ONLY_CLR = chalk.yellow
|
||||
const PROD_DEP_CLR = (s: string) => s // just use the default color
|
||||
@@ -838,3 +839,138 @@ ${DEPENDENCIES}
|
||||
└─┬ @scope/b ${VERSION_CLR('link:packages/b')}
|
||||
└── @scope/c ${VERSION_CLR('link:packages/c')}`)
|
||||
})
|
||||
|
||||
test('renderTree displays npm: protocol for aliased packages', async () => {
|
||||
const testPath = '/test/path'
|
||||
const output = await renderTree(
|
||||
[
|
||||
{
|
||||
name: 'test-project',
|
||||
path: testPath,
|
||||
version: '1.0.0',
|
||||
dependencies: [
|
||||
{
|
||||
alias: 'foo',
|
||||
name: '@pnpm.e2e/pkg-with-1-dep',
|
||||
version: '100.0.0',
|
||||
path: '/test/path/node_modules/.pnpm/@pnpm.e2e+pkg-with-1-dep@100.0.0/node_modules/@pnpm.e2e/pkg-with-1-dep',
|
||||
isMissing: false,
|
||||
isPeer: false,
|
||||
isSkipped: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
alwaysPrintRootPackage: false,
|
||||
depth: 0,
|
||||
long: false,
|
||||
search: false,
|
||||
showExtraneous: false,
|
||||
}
|
||||
)
|
||||
|
||||
// renderTree uses chalk for coloring, so we check parts separately
|
||||
expect(output).toContain('foo')
|
||||
expect(output).toContain('npm:@pnpm.e2e/pkg-with-1-dep@100.0.0')
|
||||
})
|
||||
|
||||
test('renderTree displays file: protocol correctly for aliased packages', async () => {
|
||||
const testPath = '/test/path'
|
||||
const output = await renderTree(
|
||||
[
|
||||
{
|
||||
name: 'test-project',
|
||||
path: testPath,
|
||||
version: '1.0.0',
|
||||
dependencies: [
|
||||
{
|
||||
alias: 'my-alias',
|
||||
name: 'my-local-pkg',
|
||||
version: 'my-local-pkg@file:local-pkg',
|
||||
path: '/test/path/local-pkg',
|
||||
isMissing: false,
|
||||
isPeer: false,
|
||||
isSkipped: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
alwaysPrintRootPackage: false,
|
||||
depth: 0,
|
||||
long: false,
|
||||
search: false,
|
||||
showExtraneous: false,
|
||||
}
|
||||
)
|
||||
|
||||
// renderTree uses chalk for coloring, so we check parts separately
|
||||
// instead of matching the complete string with color codes
|
||||
expect(output).toContain('my-alias')
|
||||
expect(output).toContain('my-local-pkg@file:local-pkg')
|
||||
})
|
||||
|
||||
test('renderParseable displays npm: protocol for aliased packages', async () => {
|
||||
const testPath = '/test/path'
|
||||
const output = await renderParseable(
|
||||
[
|
||||
{
|
||||
name: 'test-project',
|
||||
path: testPath,
|
||||
version: '1.0.0',
|
||||
dependencies: [
|
||||
{
|
||||
alias: 'foo',
|
||||
name: '@pnpm.e2e/pkg-with-1-dep',
|
||||
version: '100.0.0',
|
||||
path: '/test/path/node_modules/.pnpm/@pnpm.e2e+pkg-with-1-dep@100.0.0/node_modules/@pnpm.e2e/pkg-with-1-dep',
|
||||
isMissing: false,
|
||||
isPeer: false,
|
||||
isSkipped: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
alwaysPrintRootPackage: false,
|
||||
depth: 0,
|
||||
long: true,
|
||||
search: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(output).toContain('foo npm:@pnpm.e2e/pkg-with-1-dep@100.0.0')
|
||||
})
|
||||
|
||||
test('renderParseable displays file: protocol correctly for aliased packages', async () => {
|
||||
const testPath = '/test/path'
|
||||
const output = await renderParseable(
|
||||
[
|
||||
{
|
||||
name: 'test-project',
|
||||
path: testPath,
|
||||
version: '1.0.0',
|
||||
dependencies: [
|
||||
{
|
||||
alias: 'my-alias',
|
||||
name: 'my-local-pkg',
|
||||
version: 'my-local-pkg@file:local-pkg',
|
||||
path: '/test/path/local-pkg',
|
||||
isMissing: false,
|
||||
isPeer: false,
|
||||
isSkipped: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
alwaysPrintRootPackage: false,
|
||||
depth: 0,
|
||||
long: true,
|
||||
search: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(output).toContain('my-alias my-local-pkg@file:local-pkg')
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { type PnpmError } from '@pnpm/error'
|
||||
import { why } from '@pnpm/plugin-commands-listing'
|
||||
import { prepare } from '@pnpm/prepare'
|
||||
@@ -51,3 +52,93 @@ dependencies:
|
||||
@pnpm.e2e/pkg-with-1-dep 100.0.0
|
||||
└── @pnpm.e2e/dep-of-pkg-with-1-dep 100.0.0`)
|
||||
})
|
||||
|
||||
test('"why" should find packages by alias name when using npm: protocol', async () => {
|
||||
prepare({
|
||||
dependencies: {
|
||||
foo: 'npm:@pnpm.e2e/pkg-with-1-dep@100.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
await execa('node', [pnpmBin, 'install', '--registry', `http://localhost:${REGISTRY_MOCK_PORT}`])
|
||||
|
||||
const output = await why.handler({
|
||||
dev: false,
|
||||
dir: process.cwd(),
|
||||
optional: false,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
}, ['foo'])
|
||||
|
||||
const lines = stripAnsi(output).split('\n')
|
||||
expect(lines).toContain('foo npm:@pnpm.e2e/pkg-with-1-dep@100.0.0')
|
||||
})
|
||||
|
||||
test('"why" should find packages by actual package name when using npm: protocol', async () => {
|
||||
prepare({
|
||||
dependencies: {
|
||||
foo: 'npm:@pnpm.e2e/pkg-with-1-dep@100.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
await execa('node', [pnpmBin, 'install', '--registry', `http://localhost:${REGISTRY_MOCK_PORT}`])
|
||||
|
||||
const output = await why.handler({
|
||||
dev: false,
|
||||
dir: process.cwd(),
|
||||
optional: false,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
}, ['@pnpm.e2e/pkg-with-1-dep'])
|
||||
|
||||
const lines = stripAnsi(output).split('\n')
|
||||
expect(lines).toContain('foo npm:@pnpm.e2e/pkg-with-1-dep@100.0.0')
|
||||
})
|
||||
|
||||
test('"why" should display npm: protocol in parseable format', async () => {
|
||||
prepare({
|
||||
dependencies: {
|
||||
foo: 'npm:@pnpm.e2e/pkg-with-1-dep@100.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
await execa('node', [pnpmBin, 'install', '--registry', `http://localhost:${REGISTRY_MOCK_PORT}`])
|
||||
|
||||
const output = await why.handler({
|
||||
dev: false,
|
||||
dir: process.cwd(),
|
||||
optional: false,
|
||||
long: true,
|
||||
parseable: true,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
}, ['foo'])
|
||||
|
||||
const lines = output.split('\n')
|
||||
expect(lines.some(line => line.includes('foo npm:@pnpm.e2e/pkg-with-1-dep@100.0.0'))).toBe(true)
|
||||
})
|
||||
|
||||
test('"why" should display file: protocol correctly for aliased packages', async () => {
|
||||
prepare({
|
||||
dependencies: {
|
||||
'my-alias': 'file:./local-pkg',
|
||||
},
|
||||
})
|
||||
|
||||
// Create a local package after prepare() changes directory
|
||||
const localPkgDir = path.join(process.cwd(), 'local-pkg')
|
||||
fs.mkdirSync(localPkgDir, { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(localPkgDir, 'package.json'),
|
||||
JSON.stringify({ name: 'my-local-pkg', version: '1.0.0' })
|
||||
)
|
||||
|
||||
await execa('node', [pnpmBin, 'install'])
|
||||
|
||||
const output = await why.handler({
|
||||
dev: false,
|
||||
dir: process.cwd(),
|
||||
optional: false,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
}, ['my-local-pkg'])
|
||||
|
||||
const lines = stripAnsi(output).split('\n')
|
||||
expect(lines).toContain('my-alias my-local-pkg@file:local-pkg')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user