Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/cold-buckets-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pnpm/tools.plugin-commands-self-updater": patch
"pnpm": patch
---

`pnpm self-update` should download pnpm from the configured npm registry [#10205](https://github.com/pnpm/pnpm/pull/10205).
6 changes: 6 additions & 0 deletions .changeset/large-suits-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pnpm/read-project-manifest": patch
"pnpm": patch
---

Node.js runtime is not added to "dependencies" on `pnpm add`, if there's a `engines.runtime` setting declared in `package.json` [#10209](https://github.com/pnpm/pnpm/issues/10209).
9 changes: 9 additions & 0 deletions .changeset/shaky-lines-sit.md
Original file line number Diff line number Diff line change
@@ -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:[email protected]`) [#8660](https://github.com/pnpm/pnpm/issues/8660).
1 change: 1 addition & 0 deletions packages/types/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"engines": {
"runtime": {
"name": "node",
"version": "24",
"onFail": "download"
}
}
}
48 changes: 29 additions & 19 deletions pkg-manifest/read-project-manifest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,41 +229,51 @@ function convertManifestAfterRead (manifest: ProjectManifest): ProjectManifest {
}

function convertManifestBeforeWrite (manifest: ProjectManifest): ProjectManifest {
convertDependenciesToEnginesRuntime(manifest, 'devDependencies', 'devEngines')
convertDependenciesToEnginesRuntime(manifest, 'dependencies', 'engines')
return manifest
}

function convertDependenciesToEnginesRuntime (
manifest: ProjectManifest,
dependenciesFieldName: 'dependencies' | 'devDependencies',
enginesFieldName: 'engines' | 'devEngines'
): void {
for (const runtimeName of ['node', 'deno', 'bun']) {
const nodeDep = manifest.devDependencies?.[runtimeName]
if (typeof nodeDep === 'string' && nodeDep.startsWith('runtime:')) {
const version = nodeDep.replace(/^runtime:/, '')
manifest.devEngines ??= {}
const dep = manifest[dependenciesFieldName]?.[runtimeName]
if (typeof dep === 'string' && dep.startsWith('runtime:')) {
const version = dep.replace(/^runtime:/, '')
manifest[enginesFieldName] ??= {}

const nodeRuntimeEntry: EngineDependency = {
const runtimeEntry: EngineDependency = {
name: runtimeName,
version,
onFail: 'download',
}

if (!manifest.devEngines.runtime) {
manifest.devEngines.runtime = nodeRuntimeEntry
} else if (Array.isArray(manifest.devEngines.runtime)) {
const existing = manifest.devEngines.runtime.find(({ name }) => name === runtimeName)
const enginesField = manifest[enginesFieldName]!
if (!enginesField.runtime) {
enginesField.runtime = runtimeEntry
} else if (Array.isArray(enginesField.runtime)) {
const existing = enginesField.runtime.find(({ name }) => name === runtimeName)
if (existing) {
Object.assign(existing, nodeRuntimeEntry)
Object.assign(existing, runtimeEntry)
} else {
manifest.devEngines.runtime.push(nodeRuntimeEntry)
enginesField.runtime.push(runtimeEntry)
}
} else if (manifest.devEngines.runtime.name === runtimeName) {
Object.assign(manifest.devEngines.runtime, nodeRuntimeEntry)
} else if (enginesField.runtime.name === runtimeName) {
Object.assign(enginesField.runtime, runtimeEntry)
} else {
manifest.devEngines.runtime = [
manifest.devEngines.runtime,
nodeRuntimeEntry,
enginesField.runtime = [
enginesField.runtime,
runtimeEntry,
]
}
if (manifest.devDependencies) {
delete manifest.devDependencies[runtimeName]
if (manifest[dependenciesFieldName]) {
delete manifest[dependenciesFieldName][runtimeName]
}
}
}
return manifest
}

const dependencyKeys = new Set([
Expand Down
31 changes: 31 additions & 0 deletions pkg-manifest/read-project-manifest/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,37 @@ test('readProjectManifest() converts devEngines runtime to devDependencies', asy
})
})

test('readProjectManifest() converts engines runtime to dependencies', async () => {
const dir = f.prepare('package-json-with-engines')
const { manifest, writeProjectManifest } = await tryReadProjectManifest(dir)
expect(manifest).toStrictEqual(
{
dependencies: {
node: 'runtime:24',
},
engines: {
runtime: {
name: 'node',
version: '24',
onFail: 'download',
},
},
}
)
await writeProjectManifest(manifest!)
const pkgJson = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
expect(pkgJson).toStrictEqual({
dependencies: {},
engines: {
runtime: {
name: 'node',
version: '24',
onFail: 'download',
},
},
})
})

test.each([
{
name: 'creates devEngines when it is missing',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ async function dependenciesHierarchyForPackage (
})
let newEntry: PackageNode | null = null
const matchedSearched = opts.search?.({
alias,
name: packageInfo.name,
version: packageInfo.version,
readManifest,
Expand Down Expand Up @@ -231,6 +232,7 @@ async function dependenciesHierarchyForPackage (
version,
}
const matchedSearched = opts.search?.({
alias: pkg.alias,
name: pkg.name,
version: pkg.version,
readManifest: () => readPackageJsonFromDirSync(pkgPath),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions reviewing/dependencies-hierarchy/src/getTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ function getTreeHelper (
})
let circular: boolean
const matchedSearched = opts.search?.({
alias,
name: packageInfo.name,
version: packageInfo.version,
readManifest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 16 additions & 1 deletion reviewing/list/src/renderParseable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,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 [
Expand All @@ -65,6 +79,7 @@ function renderParseableForPackage (
}

interface PackageInfo {
alias: string
name: string
version: string
path: string
Expand Down
13 changes: 12 additions & 1 deletion reviewing/list/src/renderTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,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'
}
Expand Down
136 changes: 136 additions & 0 deletions reviewing/list/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/@[email protected]/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/[email protected]')
})

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/@[email protected]/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/[email protected]')
})

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')
})
Loading
Loading