feat: don't add linked in deps to the lockfile when excludeLinksFromLockfile is set to true (#6369)

This commit is contained in:
Zoltan Kochan
2023-04-09 17:41:00 +03:00
committed by GitHub
parent 080fee0b8b
commit 72ba638e34
11 changed files with 459 additions and 309 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/lockfile-utils": major
---
Breaking changes to the API of `satisfiesPackageManifest`.

View File

@@ -0,0 +1,7 @@
---
"@pnpm/resolve-dependencies": minor
"@pnpm/headless": minor
"@pnpm/core": minor
---
When `excludeLinksFromLockfile` is set to `true`, linked dependencies are not added to the lockfile.

View File

@@ -1,20 +1,22 @@
import { type Lockfile } from '@pnpm/lockfile-types'
import { type ProjectSnapshot } from '@pnpm/lockfile-types'
import {
DEPENDENCIES_FIELDS,
type ProjectManifest,
} from '@pnpm/types'
import equals from 'ramda/src/equals'
import pickBy from 'ramda/src/pickBy'
import omit from 'ramda/src/omit'
export function satisfiesPackageManifest (
lockfile: Lockfile,
pkg: ProjectManifest,
importerId: string,
opts?: { autoInstallPeers?: boolean }
opts: {
autoInstallPeers?: boolean
excludeLinksFromLockfile?: boolean
},
importer: ProjectSnapshot | undefined,
pkg: ProjectManifest
) {
const importer = lockfile.importers[importerId]
if (!importer) return false
let existingDeps = { ...pkg.devDependencies, ...pkg.dependencies, ...pkg.optionalDependencies }
let existingDeps: Record<string, string> = { ...pkg.devDependencies, ...pkg.dependencies, ...pkg.optionalDependencies }
if (opts?.autoInstallPeers) {
pkg = {
...pkg,
@@ -28,6 +30,10 @@ export function satisfiesPackageManifest (
...existingDeps,
}
}
const pickNonLinkedDeps = pickBy((spec) => !spec.startsWith('link:'))
if (opts?.excludeLinksFromLockfile) {
existingDeps = pickNonLinkedDeps(existingDeps)
}
if (
!equals(existingDeps, importer.specifiers) ||
importer.publishDirectory !== pkg.publishConfig?.directory
@@ -37,7 +43,10 @@ export function satisfiesPackageManifest (
if (!equals(pkg.dependenciesMeta ?? {}, importer.dependenciesMeta ?? {})) return false
for (const depField of DEPENDENCIES_FIELDS) {
const importerDeps = importer[depField] ?? {}
const pkgDeps = pkg[depField] ?? {}
let pkgDeps: Record<string, string> = pkg[depField] ?? {}
if (opts?.excludeLinksFromLockfile) {
pkgDeps = pickNonLinkedDeps(pkgDeps)
}
let pkgDepNames!: string[]
switch (depField) {

View File

@@ -1,130 +1,113 @@
import { satisfiesPackageManifest } from '@pnpm/lockfile-utils'
const DEFAULT_LOCKFILE_FIELDS = {
lockfileVersion: 3,
}
const DEFAULT_PKG_FIELDS = {
name: 'project',
version: '1.0.0',
}
test('satisfiesPackageManifest()', () => {
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
expect(satisfiesPackageManifest(
{},
{
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0' },
}, '.')).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: { foo: '1.0.0' },
devDependencies: {},
specifiers: { foo: '^1.0.0' },
},
{
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0' },
}
)).toBe(true)
expect(satisfiesPackageManifest(
{},
{
dependencies: { foo: '1.0.0' },
devDependencies: {},
specifiers: { foo: '^1.0.0' },
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0' },
}, '.')).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
devDependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
{
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0' },
}
)).toBe(true)
expect(satisfiesPackageManifest(
{},
{
devDependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
}, {
...DEFAULT_PKG_FIELDS,
devDependencies: { foo: '^1.0.0' },
}, '.')).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
optionalDependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
{
...DEFAULT_PKG_FIELDS,
devDependencies: { foo: '^1.0.0' },
}
)).toBe(true)
expect(satisfiesPackageManifest(
{},
{
optionalDependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
}, {
...DEFAULT_PKG_FIELDS,
optionalDependencies: { foo: '^1.0.0' },
}, '.')).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
{
...DEFAULT_PKG_FIELDS,
optionalDependencies: { foo: '^1.0.0' },
}
)).toBe(true)
expect(satisfiesPackageManifest(
{},
{
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
}, {
...DEFAULT_PKG_FIELDS,
optionalDependencies: { foo: '^1.0.0' },
}, '.')).toBe(false)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
{
...DEFAULT_PKG_FIELDS,
optionalDependencies: { foo: '^1.0.0' },
}
)).toBe(false)
expect(satisfiesPackageManifest(
{},
{
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.1.0' },
}, '.')).toBe(false)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
{
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.1.0' },
}
)).toBe(false)
expect(satisfiesPackageManifest(
{},
{
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0', bar: '2.0.0' },
}, '.')).toBe(false)
{
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0', bar: '2.0.0' },
}
)).toBe(false)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0', bar: '2.0.0' },
},
expect(satisfiesPackageManifest(
{},
{
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0', bar: '2.0.0' },
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0', bar: '2.0.0' },
}, '.')).toBe(false)
{
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0', bar: '2.0.0' },
}
)).toBe(false)
{
const lockfile = {
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: {
foo: '1.0.0',
},
optionalDependencies: {
bar: '2.0.0',
},
specifiers: {
bar: '2.0.0',
foo: '^1.0.0',
},
},
const importer = {
dependencies: {
foo: '1.0.0',
},
optionalDependencies: {
bar: '2.0.0',
},
specifiers: {
bar: '2.0.0',
foo: '^1.0.0',
},
}
const pkg = {
@@ -137,23 +120,18 @@ test('satisfiesPackageManifest()', () => {
bar: '2.0.0',
},
}
expect(satisfiesPackageManifest(lockfile, pkg, '.')).toBe(true)
expect(satisfiesPackageManifest({}, importer, pkg)).toBe(true)
}
{
const lockfile = {
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: {
bar: '2.0.0',
qar: '1.0.0',
},
specifiers: {
bar: '2.0.0',
qar: '^1.0.0',
},
},
const importer = {
dependencies: {
bar: '2.0.0',
qar: '1.0.0',
},
specifiers: {
bar: '2.0.0',
qar: '^1.0.0',
},
}
const pkg = {
@@ -162,22 +140,17 @@ test('satisfiesPackageManifest()', () => {
bar: '2.0.0',
},
}
expect(satisfiesPackageManifest(lockfile, pkg, '.')).toBe(false)
expect(satisfiesPackageManifest({}, importer, pkg)).toBe(false)
}
{
const lockfile = {
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: {
bar: '2.0.0',
qar: '1.0.0',
},
specifiers: {
bar: '2.0.0',
},
},
const importer = {
dependencies: {
bar: '2.0.0',
qar: '1.0.0',
},
specifiers: {
bar: '2.0.0',
},
}
const pkg = {
@@ -186,185 +159,195 @@ test('satisfiesPackageManifest()', () => {
bar: '2.0.0',
},
}
expect(satisfiesPackageManifest(lockfile, pkg, '.')).toBe(false)
expect(satisfiesPackageManifest({}, importer, pkg)).toBe(false)
}
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: { foo: '1.0.0', linked: 'link:../linked' },
specifiers: { foo: '^1.0.0' },
},
expect(satisfiesPackageManifest(
{},
{
dependencies: { foo: '1.0.0', linked: 'link:../linked' },
specifiers: { foo: '^1.0.0' },
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0' },
}, '.')).toBe(true)
{
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0' },
}
)).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'packages/foo': {
dependencies: { foo: '1.0.0' },
specifiers: { foo: '^1.0.0' },
},
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0' },
}, '.')).toBe(false)
expect(satisfiesPackageManifest(
{},
undefined,
{
...DEFAULT_PKG_FIELDS,
dependencies: { foo: '^1.0.0' },
}
)).toBe(false)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
expect(satisfiesPackageManifest(
{},
{
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
devDependencies: {
foo: '1.0.0',
},
}, '.')).toBe(true)
{
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
devDependencies: {
foo: '1.0.0',
},
}
)).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
expect(satisfiesPackageManifest(
{},
{
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
devDependencies: {
foo: '1.0.0',
},
dependenciesMeta: {},
}, '.')).toBe(true)
{
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
devDependencies: {
foo: '1.0.0',
},
dependenciesMeta: {},
}
)).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: {
foo: '1.0.0',
bar: '1.0.0',
},
specifiers: {
foo: '1.0.0',
bar: '^1.0.0',
},
expect(satisfiesPackageManifest(
{ autoInstallPeers: true },
{
dependencies: {
foo: '1.0.0',
bar: '1.0.0',
},
specifiers: {
foo: '1.0.0',
bar: '^1.0.0',
},
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
peerDependencies: {
bar: '^1.0.0',
},
}, '.', { autoInstallPeers: true })).toBe(true)
{
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
peerDependencies: {
bar: '^1.0.0',
},
}
)).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: {
qar: '1.0.0',
},
optionalDependencies: {
bar: '1.0.0',
},
devDependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
bar: '1.0.0',
qar: '1.0.0',
},
expect(satisfiesPackageManifest(
{ autoInstallPeers: true },
{
dependencies: {
qar: '1.0.0',
},
optionalDependencies: {
bar: '1.0.0',
},
devDependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
bar: '1.0.0',
qar: '1.0.0',
},
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: {
qar: '1.0.0',
},
optionalDependencies: {
bar: '1.0.0',
},
devDependencies: {
foo: '1.0.0',
},
peerDependencies: {
foo: '^1.0.0',
bar: '^1.0.0',
qar: '^1.0.0',
},
}, '.', { autoInstallPeers: true })).toBe(true)
{
...DEFAULT_PKG_FIELDS,
dependencies: {
qar: '1.0.0',
},
optionalDependencies: {
bar: '1.0.0',
},
devDependencies: {
foo: '1.0.0',
},
peerDependencies: {
foo: '^1.0.0',
bar: '^1.0.0',
qar: '^1.0.0',
},
}
)).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
publishDirectory: 'dist',
expect(satisfiesPackageManifest(
{},
{
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
publishDirectory: 'dist',
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
publishConfig: {
directory: 'dist',
},
}, '.')).toBe(true)
{
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
publishConfig: {
directory: 'dist',
},
}
)).toBe(true)
expect(satisfiesPackageManifest({
...DEFAULT_LOCKFILE_FIELDS,
importers: {
'.': {
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
publishDirectory: 'dist',
expect(satisfiesPackageManifest(
{},
{
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
publishDirectory: 'dist',
},
{
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
publishConfig: {
directory: 'lib',
},
}
)).toBe(false)
expect(satisfiesPackageManifest(
{
excludeLinksFromLockfile: true,
},
{
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
},
}, {
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
},
publishConfig: {
directory: 'lib',
},
}, '.')).toBe(false)
{
...DEFAULT_PKG_FIELDS,
dependencies: {
foo: '1.0.0',
bar: 'link:../bar',
},
}
)).toBe(true)
})

View File

@@ -20,13 +20,17 @@ export async function allProjectsAreUpToDate (
projects: Array<ProjectOptions & { id: string }>,
opts: {
autoInstallPeers: boolean
excludeLinksFromLockfile: boolean
linkWorkspacePackages: boolean
wantedLockfile: Lockfile
workspacePackages: WorkspacePackages
}
) {
const manifestsByDir = opts.workspacePackages ? getWorkspacePackagesByDirectory(opts.workspacePackages) : {}
const _satisfiesPackageManifest = satisfiesPackageManifest.bind(null, opts.wantedLockfile)
const _satisfiesPackageManifest = satisfiesPackageManifest.bind(null, {
autoInstallPeers: opts.autoInstallPeers,
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
})
const _linkedPackagesAreUpToDate = linkedPackagesAreUpToDate.bind(null, {
linkWorkspacePackages: opts.linkWorkspacePackages,
manifestsByDir,
@@ -35,7 +39,7 @@ export async function allProjectsAreUpToDate (
return pEvery(projects, (project) => {
const importer = opts.wantedLockfile.importers[project.id]
return !hasLocalTarballDepsInRoot(importer) &&
_satisfiesPackageManifest(project.manifest, project.id, { autoInstallPeers: opts.autoInstallPeers }) &&
_satisfiesPackageManifest(importer, project.manifest) &&
_linkedPackagesAreUpToDate({
dir: project.rootDir,
manifest: project.manifest,

View File

@@ -120,6 +120,7 @@ export interface StrictInstallOptions {
dedupeDirectDeps: boolean
dedupePeerDependents: boolean
extendNodePath: boolean
excludeLinksFromLockfile: boolean
}
export type InstallOptions =
@@ -207,6 +208,7 @@ const defaults = async (opts: InstallOptions) => {
resolvePeersFromWorkspaceRoot: true,
extendNodePath: true,
ignoreWorkspaceCycles: false,
excludeLinksFromLockfile: false,
} as StrictInstallOptions
}

View File

@@ -345,6 +345,7 @@ export async function mutateModules (
) &&
await allProjectsAreUpToDate(Object.values(ctx.projects), {
autoInstallPeers: opts.autoInstallPeers,
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
linkWorkspacePackages: opts.linkWorkspacePackagesDepth >= 0,
wantedLockfile: ctx.wantedLockfile,
workspacePackages: opts.workspacePackages,
@@ -892,6 +893,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
dedupePeerDependents: opts.dedupePeerDependents,
dryRun: opts.lockfileOnly,
engineStrict: opts.engineStrict,
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
force: opts.force,
forceFullResolution,
ignoreScripts: opts.ignoreScripts,

View File

@@ -33,6 +33,7 @@ test('allProjectsAreUpToDate(): works with packages linked through the workspace
},
], {
autoInstallPeers: false,
excludeLinksFromLockfile: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
@@ -74,6 +75,7 @@ test('allProjectsAreUpToDate(): works with aliased local dependencies', async ()
},
], {
autoInstallPeers: false,
excludeLinksFromLockfile: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
@@ -115,6 +117,7 @@ test('allProjectsAreUpToDate(): works with aliased local dependencies that speci
},
], {
autoInstallPeers: false,
excludeLinksFromLockfile: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
@@ -156,6 +159,7 @@ test('allProjectsAreUpToDate(): returns false if the aliased dependency version
},
], {
autoInstallPeers: false,
excludeLinksFromLockfile: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
@@ -230,6 +234,7 @@ test('allProjectsAreUpToDate(): use link and registry version if linkWorkspacePa
],
{
autoInstallPeers: false,
excludeLinksFromLockfile: false,
linkWorkspacePackages: false,
wantedLockfile: {
importers: {
@@ -296,6 +301,7 @@ test('allProjectsAreUpToDate(): returns false if dependenciesMeta differs', asyn
},
], {
autoInstallPeers: false,
excludeLinksFromLockfile: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
@@ -342,6 +348,7 @@ test('allProjectsAreUpToDate(): returns true if dependenciesMeta matches', async
},
], {
autoInstallPeers: false,
excludeLinksFromLockfile: false,
linkWorkspacePackages: true,
wantedLockfile: {
importers: {

View File

@@ -0,0 +1,109 @@
import fs from 'fs'
import path from 'path'
import { WANTED_LOCKFILE } from '@pnpm/constants'
import {
mutateModules,
type MutatedProject,
type ProjectOptions,
} from '@pnpm/core'
import { type LockfileV6 } from '@pnpm/lockfile-types'
import { preparePackages, tempDir } from '@pnpm/prepare'
import rimraf from '@zkochan/rimraf'
import readYamlFile from 'read-yaml-file'
import { testDefaults } from '../utils'
test('links are not added to the lockfile when excludeLinksFromLockfile is true', async () => {
const externalPkg1 = tempDir(false)
const externalPkg2 = tempDir(false)
const externalPkg3 = tempDir(false)
preparePackages([
{
location: 'project-1',
package: { name: 'project-1' },
},
{
location: 'project-2',
package: { name: 'project-2' },
},
])
const importers: MutatedProject[] = [
{
mutation: 'install',
rootDir: path.resolve('project-1'),
},
{
mutation: 'install',
rootDir: path.resolve('project-2'),
},
]
const project1Dir = path.resolve('project-1')
const project2Dir = path.resolve('project-2')
const allProjects: ProjectOptions[] = [
{
buildIndex: 0,
manifest: {
name: 'project-1',
version: '1.0.0',
dependencies: {
'is-positive': '1.0.0',
'external-1': `link:${path.relative(project1Dir, externalPkg1)}`,
},
},
rootDir: project1Dir,
},
{
buildIndex: 0,
manifest: {
name: 'project-2',
version: '1.0.0',
dependencies: {
'is-negative': '1.0.0',
'external-2': `link:${path.relative(project2Dir, externalPkg2)}`,
},
},
rootDir: project2Dir,
},
]
await mutateModules(importers, await testDefaults({ allProjects, excludeLinksFromLockfile: true }))
const lockfile: LockfileV6 = await readYamlFile(WANTED_LOCKFILE)
expect(lockfile.importers['project-1'].dependencies?.['external-1']).toBeUndefined()
expect(lockfile.importers['project-2'].dependencies?.['external-2']).toBeUndefined()
expect(fs.existsSync(path.resolve('project-1/node_modules/external-1'))).toBeTruthy()
expect(fs.existsSync(path.resolve('project-2/node_modules/external-2'))).toBeTruthy()
await rimraf('node_modules')
await rimraf('project-1/node_modules')
await rimraf('project-2/node_modules')
await mutateModules(importers, await testDefaults({ allProjects, excludeLinksFromLockfile: true, frozenLockfile: true }))
expect(lockfile.importers['project-1'].dependencies?.['external-1']).toBeUndefined()
expect(lockfile.importers['project-2'].dependencies?.['external-2']).toBeUndefined()
expect(fs.existsSync(path.resolve('project-1/node_modules/external-1'))).toBeTruthy()
expect(fs.existsSync(path.resolve('project-2/node_modules/external-2'))).toBeTruthy()
await rimraf('node_modules')
await rimraf('project-1/node_modules')
await rimraf('project-2/node_modules')
await mutateModules(importers, await testDefaults({ allProjects, excludeLinksFromLockfile: true, frozenLockfile: false, preferFrozenLockfile: false }))
expect(lockfile.importers['project-1'].dependencies?.['external-1']).toBeUndefined()
expect(lockfile.importers['project-2'].dependencies?.['external-2']).toBeUndefined()
expect(fs.existsSync(path.resolve('project-1/node_modules/external-1'))).toBeTruthy()
expect(fs.existsSync(path.resolve('project-2/node_modules/external-2'))).toBeTruthy()
delete allProjects[1].manifest.dependencies!['external-2']
allProjects[1].manifest.dependencies!['external-3'] = `link:${path.relative(project2Dir, externalPkg3)}`
await mutateModules(importers, await testDefaults({ allProjects, excludeLinksFromLockfile: true }))
expect(lockfile.importers['project-1'].dependencies?.['external-1']).toBeUndefined()
expect(lockfile.importers['project-2'].dependencies?.['external-2']).toBeUndefined()
expect(lockfile.importers['project-2'].dependencies?.['external-3']).toBeUndefined()
expect(fs.existsSync(path.resolve('project-1/node_modules/external-1'))).toBeTruthy()
// expect(fs.existsSync(path.resolve('project-2/node_modules/external-2'))).toBeFalsy() // Should we remove external links that are not in deps anymore?
expect(fs.existsSync(path.resolve('project-2/node_modules/external-3'))).toBeTruthy()
})

View File

@@ -56,7 +56,7 @@ import {
type StoreController,
} from '@pnpm/store-controller-types'
import { symlinkDependency } from '@pnpm/symlink-dependency'
import { type DependencyManifest, type HoistedDependencies, type ProjectManifest, type Registries } from '@pnpm/types'
import { type DependencyManifest, type HoistedDependencies, type ProjectManifest, type Registries, DEPENDENCIES_FIELDS } from '@pnpm/types'
import * as dp from '@pnpm/dependency-path'
import pLimit from 'p-limit'
import pathAbsolute from 'path-absolute'
@@ -104,6 +104,7 @@ export interface HeadlessOptions {
dedupeDirectDeps?: boolean
enablePnp?: boolean
engineStrict: boolean
excludeLinksFromLockfile?: boolean
extraBinPaths?: string[]
extraEnv?: Record<string, string>
extraNodePaths?: string[]
@@ -184,8 +185,12 @@ export async function headlessInstall (opts: HeadlessOptions) {
const selectedProjects = Object.values(pick(opts.selectedProjectDirs, opts.allProjects))
if (!opts.ignorePackageManifest) {
const _satisfiesPackageManifest = satisfiesPackageManifest.bind(null, {
autoInstallPeers: opts.autoInstallPeers,
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
})
for (const { id, manifest, rootDir } of selectedProjects) {
if (!satisfiesPackageManifest(wantedLockfile, manifest, id, { autoInstallPeers: opts.autoInstallPeers })) {
if (!_satisfiesPackageManifest(wantedLockfile.importers[id], manifest)) {
throw new PnpmError('OUTDATED_LOCKFILE',
`Cannot install with "frozen-lockfile" because ${WANTED_LOCKFILE} is not up to date with ` +
path.relative(lockfileDir, path.join(rootDir, 'package.json')), {
@@ -260,6 +265,22 @@ export async function headlessInstall (opts: HeadlessOptions) {
includeIncompatiblePackages: opts.force,
lockfileDir,
})
if (opts.excludeLinksFromLockfile) {
for (const { id, manifest } of selectedProjects) {
if (filteredLockfile.importers[id]) {
for (const depType of DEPENDENCIES_FIELDS) {
for (const [depName, spec] of Object.entries(manifest[depType] ?? {})) {
if (spec.startsWith('link:')) {
if (!filteredLockfile.importers[id][depType]) {
filteredLockfile.importers[id][depType] = {}
}
filteredLockfile.importers[id][depType]![depName] = spec
}
}
}
}
}
}
// Update selectedProjects to add missing projects. importerIds will have the updated ids, found from deeply linked workspace projects
const initialImporterIdSet = new Set(initialImporterIds)

View File

@@ -94,6 +94,7 @@ export async function resolveDependencies (
opts: ResolveDependenciesOptions & {
defaultUpdateDepth: number
dedupePeerDependents?: boolean
excludeLinksFromLockfile?: boolean
preserveWorkspaceProtocol: boolean
saveWorkspaceProtocol: 'rolling' | boolean
lockfileIncludeTarballUrl?: boolean
@@ -170,7 +171,7 @@ export async function resolveDependencies (
resolvedImporter.linkedDependencies,
resolvedImporter.directDependencies,
opts.registries,
opts.autoInstallPeers
opts.excludeLinksFromLockfile
)
}
@@ -354,7 +355,7 @@ function addDirectDependenciesToLockfile (
linkedPackages: Array<{ alias: string }>,
directDependencies: ResolvedDirectDependency[],
registries: Registries,
autoInstallPeers?: boolean
excludeLinksFromLockfile?: boolean
): ProjectSnapshot {
const newProjectSnapshot: ProjectSnapshot & Required<Pick<ProjectSnapshot, 'dependencies' | 'devDependencies' | 'optionalDependencies'>> = {
dependencies: {},
@@ -379,7 +380,7 @@ function addDirectDependenciesToLockfile (
const allDeps = Array.from(new Set(Object.keys(getAllDependenciesFromManifest(newManifest))))
for (const alias of allDeps) {
if (directDependenciesByAlias[alias]) {
if (directDependenciesByAlias[alias] && (!excludeLinksFromLockfile || !(directDependenciesByAlias[alias] as LinkedDependency).isLinkedDependency)) {
const dep = directDependenciesByAlias[alias]
const ref = depPathToRef(dep.pkgId, {
alias: dep.alias,