mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-28 02:53:15 -04:00
fix: detect overrides and other lockfile-affecting setting changes in optimisticRepeatInstall (#10654)
* fix: detect overrides and other lockfile-affecting setting changes in optimisticRepeatInstall When optimisticRepeatInstall was enabled, changing overrides, packageExtensions, ignoredOptionalDependencies, patchedDependencies, or peersSuffixMaxLength would not trigger a reinstall because these settings were not tracked in the workspace state file. * refactor: extract WORKSPACE_STATE_SETTING_KEYS to prevent type/runtime drift The settings key list in createWorkspaceState's pick() call must stay in sync with the WorkspaceStateSettings type. Extract a shared const array so both the type and runtime pick are derived from a single source, preventing the class of bug fixed in the previous commit.
This commit is contained in:
6
.changeset/fix-optimistic-repeat-install-overrides.md
Normal file
6
.changeset/fix-optimistic-repeat-install-overrides.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/workspace.state": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fixed `optimisticRepeatInstall` skipping install when `overrides`, `packageExtensions`, `ignoredOptionalDependencies`, `patchedDependencies`, or `peersSuffixMaxLength` changed.
|
||||
157
deps/status/test/checkDepsStatus.test.ts
vendored
157
deps/status/test/checkDepsStatus.test.ts
vendored
@@ -41,6 +41,163 @@ const lockfileFs = await import('@pnpm/lockfile.fs')
|
||||
const fsUtils = await import('../lib/safeStat.js')
|
||||
const statManifestFileUtils = await import('../lib/statManifestFile.js')
|
||||
|
||||
describe('checkDepsStatus - settings change detection', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns upToDate: false when overrides have changed', async () => {
|
||||
const lastValidatedTimestamp = Date.now() - 10_000
|
||||
const mockWorkspaceState: WorkspaceState = {
|
||||
lastValidatedTimestamp,
|
||||
pnpmfiles: [],
|
||||
settings: {
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
preferWorkspacePackages: true,
|
||||
overrides: { foo: '1.0.0' },
|
||||
},
|
||||
projects: {},
|
||||
filteredInstall: false,
|
||||
}
|
||||
|
||||
jest.mocked(loadWorkspaceState).mockReturnValue(mockWorkspaceState)
|
||||
|
||||
const opts: CheckDepsStatusOptions = {
|
||||
rootProjectManifest: {},
|
||||
rootProjectManifestDir: '/project',
|
||||
pnpmfile: [],
|
||||
...mockWorkspaceState.settings,
|
||||
overrides: { foo: '2.0.0' },
|
||||
}
|
||||
const result = await checkDepsStatus(opts)
|
||||
|
||||
expect(result.upToDate).toBe(false)
|
||||
expect(result.issue).toBe('The value of the overrides setting has changed')
|
||||
})
|
||||
|
||||
it('returns upToDate: false when packageExtensions have changed', async () => {
|
||||
const lastValidatedTimestamp = Date.now() - 10_000
|
||||
const mockWorkspaceState: WorkspaceState = {
|
||||
lastValidatedTimestamp,
|
||||
pnpmfiles: [],
|
||||
settings: {
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
preferWorkspacePackages: true,
|
||||
packageExtensions: { foo: { dependencies: { bar: '1.0.0' } } },
|
||||
},
|
||||
projects: {},
|
||||
filteredInstall: false,
|
||||
}
|
||||
|
||||
jest.mocked(loadWorkspaceState).mockReturnValue(mockWorkspaceState)
|
||||
|
||||
const opts: CheckDepsStatusOptions = {
|
||||
rootProjectManifest: {},
|
||||
rootProjectManifestDir: '/project',
|
||||
pnpmfile: [],
|
||||
...mockWorkspaceState.settings,
|
||||
packageExtensions: { foo: { dependencies: { bar: '2.0.0' } } },
|
||||
}
|
||||
const result = await checkDepsStatus(opts)
|
||||
|
||||
expect(result.upToDate).toBe(false)
|
||||
expect(result.issue).toBe('The value of the packageExtensions setting has changed')
|
||||
})
|
||||
|
||||
it('returns upToDate: false when ignoredOptionalDependencies have changed', async () => {
|
||||
const lastValidatedTimestamp = Date.now() - 10_000
|
||||
const mockWorkspaceState: WorkspaceState = {
|
||||
lastValidatedTimestamp,
|
||||
pnpmfiles: [],
|
||||
settings: {
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
preferWorkspacePackages: true,
|
||||
ignoredOptionalDependencies: ['foo'],
|
||||
},
|
||||
projects: {},
|
||||
filteredInstall: false,
|
||||
}
|
||||
|
||||
jest.mocked(loadWorkspaceState).mockReturnValue(mockWorkspaceState)
|
||||
|
||||
const opts: CheckDepsStatusOptions = {
|
||||
rootProjectManifest: {},
|
||||
rootProjectManifestDir: '/project',
|
||||
pnpmfile: [],
|
||||
...mockWorkspaceState.settings,
|
||||
ignoredOptionalDependencies: ['foo', 'bar'],
|
||||
}
|
||||
const result = await checkDepsStatus(opts)
|
||||
|
||||
expect(result.upToDate).toBe(false)
|
||||
expect(result.issue).toBe('The value of the ignoredOptionalDependencies setting has changed')
|
||||
})
|
||||
|
||||
it('returns upToDate: false when patchedDependencies have changed', async () => {
|
||||
const lastValidatedTimestamp = Date.now() - 10_000
|
||||
const mockWorkspaceState: WorkspaceState = {
|
||||
lastValidatedTimestamp,
|
||||
pnpmfiles: [],
|
||||
settings: {
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
preferWorkspacePackages: true,
|
||||
patchedDependencies: { foo: 'patches/foo.patch' },
|
||||
},
|
||||
projects: {},
|
||||
filteredInstall: false,
|
||||
}
|
||||
|
||||
jest.mocked(loadWorkspaceState).mockReturnValue(mockWorkspaceState)
|
||||
|
||||
const opts: CheckDepsStatusOptions = {
|
||||
rootProjectManifest: {},
|
||||
rootProjectManifestDir: '/project',
|
||||
pnpmfile: [],
|
||||
...mockWorkspaceState.settings,
|
||||
patchedDependencies: { foo: 'patches/foo-v2.patch' },
|
||||
}
|
||||
const result = await checkDepsStatus(opts)
|
||||
|
||||
expect(result.upToDate).toBe(false)
|
||||
expect(result.issue).toBe('The value of the patchedDependencies setting has changed')
|
||||
})
|
||||
|
||||
it('returns upToDate: false when peersSuffixMaxLength has changed', async () => {
|
||||
const lastValidatedTimestamp = Date.now() - 10_000
|
||||
const mockWorkspaceState: WorkspaceState = {
|
||||
lastValidatedTimestamp,
|
||||
pnpmfiles: [],
|
||||
settings: {
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
preferWorkspacePackages: true,
|
||||
peersSuffixMaxLength: 1000,
|
||||
},
|
||||
projects: {},
|
||||
filteredInstall: false,
|
||||
}
|
||||
|
||||
jest.mocked(loadWorkspaceState).mockReturnValue(mockWorkspaceState)
|
||||
|
||||
const opts: CheckDepsStatusOptions = {
|
||||
rootProjectManifest: {},
|
||||
rootProjectManifestDir: '/project',
|
||||
pnpmfile: [],
|
||||
...mockWorkspaceState.settings,
|
||||
peersSuffixMaxLength: 100,
|
||||
}
|
||||
const result = await checkDepsStatus(opts)
|
||||
|
||||
expect(result.upToDate).toBe(false)
|
||||
expect(result.issue).toBe('The value of the peersSuffixMaxLength setting has changed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkDepsStatus - pnpmfile modification', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { pick } from 'ramda'
|
||||
import { type ConfigDependencies } from '@pnpm/types'
|
||||
import { type WorkspaceState, type WorkspaceStateSettings, type ProjectsList } from './types.js'
|
||||
import { WORKSPACE_STATE_SETTING_KEYS, type WorkspaceState, type WorkspaceStateSettings, type ProjectsList } from './types.js'
|
||||
|
||||
export interface CreateWorkspaceStateOptions {
|
||||
allProjects: ProjectsList
|
||||
@@ -20,25 +20,7 @@ export const createWorkspaceState = (opts: CreateWorkspaceStateOptions): Workspa
|
||||
},
|
||||
])),
|
||||
pnpmfiles: opts.pnpmfiles,
|
||||
settings: pick([
|
||||
'autoInstallPeers',
|
||||
'catalogs',
|
||||
'dedupeDirectDeps',
|
||||
'dedupeInjectedDeps',
|
||||
'dedupePeerDependents',
|
||||
'dev',
|
||||
'excludeLinksFromLockfile',
|
||||
'hoistPattern',
|
||||
'hoistWorkspacePackages',
|
||||
'injectWorkspacePackages',
|
||||
'linkWorkspacePackages',
|
||||
'nodeLinker',
|
||||
'optional',
|
||||
'preferWorkspacePackages',
|
||||
'production',
|
||||
'publicHoistPattern',
|
||||
'workspacePackagePatterns',
|
||||
], opts.settings),
|
||||
settings: pick(WORKSPACE_STATE_SETTING_KEYS, opts.settings),
|
||||
filteredInstall: opts.filteredInstall,
|
||||
configDependencies: opts.configDependencies,
|
||||
})
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { loadWorkspaceState } from './loadWorkspaceState.js'
|
||||
export { type UpdateWorkspaceStateOptions, updateWorkspaceState } from './updateWorkspaceState.js'
|
||||
export { type WorkspaceState, type WorkspaceStateSettings, type ProjectsList } from './types.js'
|
||||
export { WORKSPACE_STATE_SETTING_KEYS, type WorkspaceState, type WorkspaceStateSettings, type ProjectsList } from './types.js'
|
||||
|
||||
@@ -15,22 +15,29 @@ export interface WorkspaceState {
|
||||
settings: WorkspaceStateSettings
|
||||
}
|
||||
|
||||
export type WorkspaceStateSettings = Pick<Config,
|
||||
| 'autoInstallPeers'
|
||||
| 'catalogs'
|
||||
| 'dedupeDirectDeps'
|
||||
| 'dedupeInjectedDeps'
|
||||
| 'dedupePeerDependents'
|
||||
| 'dev'
|
||||
| 'excludeLinksFromLockfile'
|
||||
| 'hoistPattern'
|
||||
| 'hoistWorkspacePackages'
|
||||
| 'injectWorkspacePackages'
|
||||
| 'linkWorkspacePackages'
|
||||
| 'nodeLinker'
|
||||
| 'optional'
|
||||
| 'preferWorkspacePackages'
|
||||
| 'production'
|
||||
| 'publicHoistPattern'
|
||||
| 'workspacePackagePatterns'
|
||||
>
|
||||
export const WORKSPACE_STATE_SETTING_KEYS = [
|
||||
'autoInstallPeers',
|
||||
'catalogs',
|
||||
'dedupeDirectDeps',
|
||||
'dedupeInjectedDeps',
|
||||
'dedupePeerDependents',
|
||||
'dev',
|
||||
'excludeLinksFromLockfile',
|
||||
'hoistPattern',
|
||||
'hoistWorkspacePackages',
|
||||
'ignoredOptionalDependencies',
|
||||
'injectWorkspacePackages',
|
||||
'linkWorkspacePackages',
|
||||
'nodeLinker',
|
||||
'optional',
|
||||
'overrides',
|
||||
'packageExtensions',
|
||||
'patchedDependencies',
|
||||
'peersSuffixMaxLength',
|
||||
'preferWorkspacePackages',
|
||||
'production',
|
||||
'publicHoistPattern',
|
||||
'workspacePackagePatterns',
|
||||
] as const satisfies ReadonlyArray<keyof Config>
|
||||
|
||||
export type WorkspaceStateSettings = Pick<Config, typeof WORKSPACE_STATE_SETTING_KEYS[number]>
|
||||
|
||||
@@ -27,6 +27,45 @@ test('createWorkspaceState() on empty list', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
test('createWorkspaceState() saves lockfile-affecting settings', () => {
|
||||
prepareEmpty()
|
||||
|
||||
const state = createWorkspaceState({
|
||||
allProjects: [],
|
||||
pnpmfiles: [],
|
||||
filteredInstall: false,
|
||||
settings: {
|
||||
autoInstallPeers: true,
|
||||
dedupeDirectDeps: true,
|
||||
excludeLinksFromLockfile: false,
|
||||
preferWorkspacePackages: false,
|
||||
linkWorkspacePackages: false,
|
||||
injectWorkspacePackages: false,
|
||||
overrides: {
|
||||
foo: '1.0.0',
|
||||
},
|
||||
packageExtensions: {
|
||||
bar: { dependencies: { baz: '2.0.0' } },
|
||||
},
|
||||
ignoredOptionalDependencies: ['qux'],
|
||||
patchedDependencies: {
|
||||
'some-pkg': 'patches/some-pkg.patch',
|
||||
},
|
||||
peersSuffixMaxLength: 100,
|
||||
},
|
||||
})
|
||||
|
||||
expect(state.settings.overrides).toStrictEqual({ foo: '1.0.0' })
|
||||
expect(state.settings.packageExtensions).toStrictEqual({
|
||||
bar: { dependencies: { baz: '2.0.0' } },
|
||||
})
|
||||
expect(state.settings.ignoredOptionalDependencies).toStrictEqual(['qux'])
|
||||
expect(state.settings.patchedDependencies).toStrictEqual({
|
||||
'some-pkg': 'patches/some-pkg.patch',
|
||||
})
|
||||
expect(state.settings.peersSuffixMaxLength).toBe(100)
|
||||
})
|
||||
|
||||
test('createWorkspaceState() on non-empty list', () => {
|
||||
preparePackages(['a', 'b', 'c', 'd'].map(name => ({
|
||||
location: `./packages/${name}`,
|
||||
|
||||
Reference in New Issue
Block a user