Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 43711ce

Browse filesBrowse files
lizujuzkochan
andauthored
fix(outdated): skip local lockfile refs (#12834)
`pnpm outdated` now skips dependencies whose lockfile ref is local (`link:`, `file:`, or `workspace:`), even when the manifest specifier is a plain semver range. This covers workspace-linked packages such as: ```yaml devDependencies: private-workspace-pkg: specifier: ^1.0.0 version: link:../private-workspace-pkg ``` In that shape the dependency is already resolved locally, so asking the registry for a latest version can fail for private workspace packages that are not published. Refs #12827. Follow-up commits after review: - The local-ref early return runs before any other per-dependency work, and the `isLocalRef` helper documents why local refs have no registry "latest". - Added a test locking in the other side of the behavior: a dependency whose *current* ref is local but whose *wanted* ref is a registry version is still checked and reported (addresses CodeRabbit's suggestion). - **pacquet parity:** pacquet's `outdated` already skips local refs — `ImporterDepVersion::ver_peer()` returns `None` for `link:`/`file:`, so `collect_outdated` drops those deps before any registry fetch. This PR adds a Rust test (`current_versions_omit_local_refs`) proving that behavior, so both stacks now cover the same scenario. No pacquet behavior change was needed; the TypeScript fix brings pnpm in line with pacquet. --------- Co-authored-by: Zoltan Kochan <zoltankochan@gmail.com>
1 parent a897ef7 commit 43711ce
Copy full SHA for 43711ce

6 files changed

+166-3Lines changed: 166 additions & 3 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file
+6Lines changed: 6 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@pnpm/deps.inspection.outdated": patch
3+
"pnpm": patch
4+
---
5+
6+
`pnpm outdated` no longer checks the registry for dependencies that are resolved from local `link:`, `file:`, or `workspace:` references in the lockfile [#12827](https://github.com/pnpm/pnpm/issues/12827).
Collapse file

‎Cargo.lock‎

Copy file name to clipboardExpand all lines: Cargo.lock
+1Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Collapse file

‎pacquet/crates/cli/Cargo.toml‎

Copy file name to clipboardExpand all lines: pacquet/crates/cli/Cargo.toml
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ mockito = { workspace = true }
102102
pretty_assertions = { workspace = true }
103103
serde-saphyr = { workspace = true }
104104
tar = { workspace = true }
105+
text-block-macros = { workspace = true }
105106
walkdir = { workspace = true }
106107

107108
[lints]
Collapse file

‎pacquet/crates/cli/src/cli_args/outdated/tests.rs‎

Copy file name to clipboardExpand all lines: pacquet/crates/cli/src/cli_args/outdated/tests.rs
+36-2Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use super::{
2-
Change, OutdatedDependencyOptions, OutdatedPackage, classify, render_json, render_latest,
3-
sort_outdated,
2+
Change, OutdatedDependencyOptions, OutdatedPackage, classify, current_versions_from_lockfile,
3+
render_json, render_latest, sort_outdated,
44
};
55
use node_semver::Version;
6+
use pacquet_lockfile::Lockfile;
67
use pacquet_package_manifest::DependencyGroup;
8+
use std::collections::HashMap;
9+
use text_block_macros::text_block;
710

811
fn v(text: &str) -> Version {
912
text.parse().expect("parse semver")
@@ -21,6 +24,37 @@ fn pkg(name: &str, current: &str, target: &str, group: DependencyGroup) -> Outda
2124
}
2225
}
2326

27+
// Mirrors `outdated() skips dependencies resolved from local refs` in
28+
// `pnpm11/deps/inspection/outdated/test/outdated.spec.ts`: a dependency
29+
// resolved to a local `link:`/`file:`/`workspace:` ref has no
30+
// lockfile-pinned semver, so `collect_outdated` drops it before any
31+
// registry fetch even when its manifest specifier is a plain semver range.
32+
#[test]
33+
fn current_versions_omit_local_refs() {
34+
let lockfile: Lockfile = serde_saphyr::from_str(text_block! {
35+
"lockfileVersion: '9.0'"
36+
"importers:"
37+
" .:"
38+
" devDependencies:"
39+
" private-workspace-pkg:"
40+
" specifier: ^1.0.0"
41+
" version: link:../private-workspace-pkg"
42+
" injected-pkg:"
43+
" specifier: ^1.0.0"
44+
" version: file:../injected-pkg"
45+
" workspace-pkg:"
46+
" specifier: ^1.0.0"
47+
" version: workspace:../workspace-pkg"
48+
" is-positive:"
49+
" specifier: ^1.0.0"
50+
" version: 1.0.0"
51+
})
52+
.expect("parse fixture lockfile");
53+
let versions = current_versions_from_lockfile(Some(&lockfile), &[DependencyGroup::Dev]);
54+
dbg!(&versions);
55+
assert_eq!(versions, HashMap::from([("is-positive".to_string(), v("1.0.0"))]));
56+
}
57+
2458
#[test]
2559
fn classify_detects_each_bump_kind() {
2660
assert_eq!(classify(&v("1.0.0"), &v("2.0.0")), Change::Breaking);
Collapse file

‎pnpm11/deps/inspection/outdated/src/outdated.ts‎

Copy file name to clipboardExpand all lines: pnpm11/deps/inspection/outdated/src/outdated.ts
+10Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export async function outdated (
114114
pkgs.map(async (alias) => {
115115
if (!allDeps[alias]) return
116116
const wantedRef = opts.wantedLockfile!.importers[importerId][depType]![alias]
117+
if (isLocalRef(wantedRef)) return
117118
if (ignoreDependenciesMatcher?.(alias)) return
118119

119120
const currentRef = (currentLockfile.importers[importerId] as ProjectSnapshot)?.[depType]?.[alias]
@@ -196,6 +197,15 @@ function isEmpty (obj: object): boolean {
196197
return Object.keys(obj).length === 0
197198
}
198199

200+
// A dependency whose wanted ref is local resolves to a directory on disk
201+
// even when its manifest specifier is a plain semver range (e.g. a
202+
// workspace package matched by `link-workspace-packages`). Such a package
203+
// may not be published at all, so there is no registry "latest" to compare
204+
// against.
205+
function isLocalRef (ref: string): boolean {
206+
return ref.startsWith('link:') || ref.startsWith('file:') || ref.startsWith('workspace:')
207+
}
208+
199209
// Pick a clean display string for a lockfile ref.
200210
//
201211
// - If the dep-path parses to a semver, that's the value (handles
Collapse file

‎pnpm11/deps/inspection/outdated/test/outdated.spec.ts‎

Copy file name to clipboardExpand all lines: pnpm11/deps/inspection/outdated/test/outdated.spec.ts
+112-1Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@jest/globals'
1+
import { expect, jest, test } from '@jest/globals'
22
import { LOCKFILE_VERSION } from '@pnpm/constants'
33
import type { ResolveLatestDispatcher } from '@pnpm/installing.client'
44
import type { DepPath, PackageManifest, ProjectId } from '@pnpm/types'
@@ -62,6 +62,117 @@ async function getLatestManifest (packageName: string): Promise<PackageManifest
6262

6363
const resolveLatest = makeResolveLatest(getLatestManifest)
6464

65+
test('outdated() skips dependencies resolved from local refs', async () => {
66+
const resolveLatest = jest.fn<ResolveLatestDispatcher>(async () => {
67+
throw new Error('local dependency should not resolve latest from the registry')
68+
})
69+
70+
const outdatedPkgs = await outdated({
71+
currentLockfile: {
72+
importers: {
73+
['.' as ProjectId]: {
74+
devDependencies: {
75+
'private-workspace-pkg': 'link:../private-workspace-pkg',
76+
},
77+
specifiers: {
78+
'private-workspace-pkg': '^1.0.0',
79+
},
80+
},
81+
},
82+
lockfileVersion: LOCKFILE_VERSION,
83+
},
84+
resolveLatest,
85+
lockfileDir: 'project',
86+
manifest: {
87+
name: 'wanted-shrinkwrap',
88+
version: '1.0.0',
89+
devDependencies: {
90+
'private-workspace-pkg': '^1.0.0',
91+
},
92+
},
93+
prefix: 'project',
94+
wantedLockfile: {
95+
importers: {
96+
['.' as ProjectId]: {
97+
devDependencies: {
98+
'private-workspace-pkg': 'link:../private-workspace-pkg',
99+
},
100+
specifiers: {
101+
'private-workspace-pkg': '^1.0.0',
102+
},
103+
},
104+
},
105+
lockfileVersion: LOCKFILE_VERSION,
106+
},
107+
})
108+
109+
expect(outdatedPkgs).toStrictEqual([])
110+
expect(resolveLatest).not.toHaveBeenCalled()
111+
})
112+
113+
test('outdated() still checks the registry when only the current ref is local', async () => {
114+
const outdatedPkgs = await outdated({
115+
currentLockfile: {
116+
importers: {
117+
['.' as ProjectId]: {
118+
devDependencies: {
119+
'is-positive': 'link:../is-positive',
120+
},
121+
specifiers: {
122+
'is-positive': '^1.0.0',
123+
},
124+
},
125+
},
126+
lockfileVersion: LOCKFILE_VERSION,
127+
},
128+
resolveLatest,
129+
lockfileDir: 'project',
130+
manifest: {
131+
name: 'wanted-shrinkwrap',
132+
version: '1.0.0',
133+
devDependencies: {
134+
'is-positive': '^1.0.0',
135+
},
136+
},
137+
prefix: 'project',
138+
wantedLockfile: {
139+
importers: {
140+
['.' as ProjectId]: {
141+
devDependencies: {
142+
'is-positive': '1.0.0',
143+
},
144+
specifiers: {
145+
'is-positive': '^1.0.0',
146+
},
147+
},
148+
},
149+
lockfileVersion: LOCKFILE_VERSION,
150+
packages: {
151+
['is-positive@1.0.0' as DepPath]: {
152+
resolution: {
153+
integrity: 'sha512-xxzPGZ4P2uN6rROUa5N9Z7zTX6ERuE0hs6GUOc/cKBLF2NqKc16UwqHMt3tFg4CO6EBTE5UecUasg+3jZx3Ckg==',
154+
},
155+
},
156+
},
157+
},
158+
})
159+
160+
expect(outdatedPkgs).toStrictEqual([
161+
{
162+
alias: 'is-positive',
163+
belongsTo: 'devDependencies',
164+
current: 'link:../is-positive',
165+
latestManifest: {
166+
name: 'is-positive',
167+
version: '3.1.0',
168+
},
169+
packageName: 'is-positive',
170+
wanted: '1.0.0',
171+
workspace: 'wanted-shrinkwrap',
172+
},
173+
])
174+
})
175+
65176
test('outdated()', async () => {
66177
const outdatedPkgs = await outdated({
67178
currentLockfile: {

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.