Files
pnpm/installing/deps-resolver/src/updateProjectManifest.ts
morning-verlu 531f2a307c fix: preserve workspace specs on update (#12140)
## What
- preserve existing `workspace:` dependency specifiers when `updateProjectManifest` saves updated direct dependencies and `preserveWorkspaceProtocol` is enabled
- keep catalog specifiers taking precedence over resolver-normalized specs
- add focused coverage for preserved and normalized local spec behavior
- add a changeset for the published `@pnpm/installing.deps-resolver` change

### pacquet parity
Ported the same fix to pacquet's `update` command. Previously `pacquet update --latest` routed every direct dependency through a registry `latest` lookup, so a `workspace:` local-path dependency (e.g. `workspace:../packages/foo/dist`) was rewritten into a registry version — corrupting the manifest (in the regression test it became `0.0.1-security`). Both `--latest` rewrite sites now skip registry resolution for such specs via `is_workspace_local_path_specifier`, a faithful port of pnpm's `isWorkspaceLocalPathSpecifier`. The gate is unconditional in the `--latest` path because `preserveWorkspaceProtocol` is always on there (its only override derives from `linkWorkspacePackages` under `--workspace`, which cannot be combined with `--latest`).

Fixes #3902

---------

Co-authored-by: morning-verlu <258725120+morning-verlu@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-16 23:38:05 +00:00

77 lines
2.7 KiB
TypeScript

import {
type PackageSpecObject,
updateProjectManifestObject,
} from '@pnpm/pkg-manifest.utils'
import type { ProjectManifest } from '@pnpm/types'
import type { ImporterToResolve } from './index.js'
import type { ResolvedDirectDependency } from './resolveDependencyTree.js'
export async function updateProjectManifest (
importer: ImporterToResolve,
opts: {
directDependencies: ResolvedDirectDependency[]
preserveWorkspaceProtocol: boolean
saveWorkspaceProtocol: boolean | 'rolling'
}
): Promise<Array<ProjectManifest | undefined>> {
if (!importer.manifest) {
throw new Error('Cannot save because no package.json found')
}
const specsToUpsert: PackageSpecObject[] = opts.directDependencies
.filter((rdd, index) => importer.wantedDependencies[index]?.updateSpec)
.map((rdd, index) => {
const wantedDep = importer.wantedDependencies[index]!
return {
alias: rdd.alias,
peer: importer.peer,
bareSpecifier: getBareSpecifierToSave(wantedDep, rdd, opts.preserveWorkspaceProtocol),
resolvedVersion: rdd.version,
pinnedVersion: importer.pinnedVersion,
saveType: importer.targetDependenciesField,
}
})
for (const pkgToInstall of importer.wantedDependencies) {
if (pkgToInstall.updateSpec && pkgToInstall.alias && !specsToUpsert.some(({ alias }) => alias === pkgToInstall.alias)) {
specsToUpsert.push({
alias: pkgToInstall.alias,
peer: importer.peer,
saveType: importer.targetDependenciesField,
})
}
}
const hookedManifest = await updateProjectManifestObject(
importer.rootDir,
importer.manifest,
specsToUpsert
)
const originalManifest = (importer.originalManifest != null)
? await updateProjectManifestObject(
importer.rootDir,
importer.originalManifest,
specsToUpsert
)
: undefined
return [hookedManifest, originalManifest]
}
function getBareSpecifierToSave (
wantedDep: ImporterToResolve['wantedDependencies'][number],
resolvedDep: ResolvedDirectDependency,
preserveWorkspaceProtocol: boolean
): string {
if (resolvedDep.catalogLookup != null) {
return resolvedDep.catalogLookup.userSpecifiedBareSpecifier
}
if (preserveWorkspaceProtocol && isWorkspaceLocalPathSpecifier(wantedDep.bareSpecifier)) {
return wantedDep.bareSpecifier
}
return resolvedDep.normalizedBareSpecifier ?? wantedDep.bareSpecifier
}
function isWorkspaceLocalPathSpecifier (bareSpecifier: string): boolean {
if (!bareSpecifier.startsWith('workspace:')) return false
const pref = bareSpecifier.slice('workspace:'.length)
return pref.startsWith('.') || pref.startsWith('/') || pref.startsWith('~/') || /^[A-Z]:/i.test(pref)
}