feat: setting config settings via pnpm-workspace.yaml (#9211)

Related discussion: https://github.com/orgs/pnpm/discussions/9037
This commit is contained in:
Zoltan Kochan
2025-03-04 15:09:13 +01:00
committed by GitHub
parent cd8caece25
commit d965748ff4
10 changed files with 76 additions and 30 deletions

View File

@@ -0,0 +1,18 @@
---
"@pnpm/plugin-commands-installation": minor
"@pnpm/plugin-commands-patching": minor
"@pnpm/config": minor
"pnpm": minor
---
`pnpm-workspace.yaml` can now hold all the settings that `.npmrc` accepts. The settings should use camelCase [#9211](https://github.com/pnpm/pnpm/pull/9211).
`pnpm-workspace.yaml` example:
```yaml
verifyDepsBeforeRun: install
optimisticRepeatInstall: true
publicHoistPattern:
- "*types*"
- "!@types/react"
```

View File

@@ -28,7 +28,20 @@ export type OptionsFromRootManifest = {
export function getOptionsFromRootManifest (manifestDir: string, manifest: ProjectManifest): OptionsFromRootManifest {
const settings: OptionsFromRootManifest = getOptionsFromPnpmSettings(manifestDir, {
...manifest.pnpm,
...pick([
'allowNonAppliedPatches',
'allowedDeprecatedVersions',
'configDependencies',
'ignoredBuiltDependencies',
'ignoredOptionalDependencies',
'neverBuiltDependencies',
'onlyBuiltDependencies',
'onlyBuiltDependenciesFile',
'overrides',
'packageExtensions',
'peerDependencyRules',
'supportedArchitectures',
], manifest.pnpm ?? {}),
// We read Yarn's resolutions field for compatibility
// but we really replace the version specs to any other version spec, not only to exact versions,
// so we cannot call it resolutions
@@ -41,20 +54,7 @@ export function getOptionsFromRootManifest (manifestDir: string, manifest: Proje
}
export function getOptionsFromPnpmSettings (manifestDir: string, pnpmSettings: PnpmSettings, manifest?: ProjectManifest): OptionsFromRootManifest {
const settings: OptionsFromRootManifest = pick([
'allowNonAppliedPatches',
'allowedDeprecatedVersions',
'configDependencies',
'ignoredBuiltDependencies',
'ignoredOptionalDependencies',
'neverBuiltDependencies',
'onlyBuiltDependencies',
'onlyBuiltDependenciesFile',
'overrides',
'packageExtensions',
'peerDependencyRules',
'supportedArchitectures',
], pnpmSettings)
const settings: OptionsFromRootManifest = { ...pnpmSettings }
if (settings.overrides) {
if (Object.keys(settings.overrides).length === 0) {
delete settings.overrides

View File

@@ -213,10 +213,13 @@ export async function getConfig (opts: {
const rcOptions = Object.keys(rcOptionsTypes)
const pnpmConfig: ConfigWithDeprecatedSettings = Object.fromEntries([
...rcOptions.map((configKey) => [camelcase(configKey, { locale: 'en-US' }), npmConfig.get(configKey)]) as any, // eslint-disable-line
...Object.entries(cliOptions).filter(([name, value]) => typeof value !== 'undefined').map(([name, value]) => [camelcase(name, { locale: 'en-US' }), value]),
]) as unknown as ConfigWithDeprecatedSettings
const configFromCliOpts = Object.fromEntries(Object.entries(cliOptions)
.filter(([_, value]) => typeof value !== 'undefined')
.map(([name, value]) => [camelcase(name, { locale: 'en-US' }), value])
)
const pnpmConfig: ConfigWithDeprecatedSettings = Object.assign(Object.fromEntries(
rcOptions.map((configKey) => [camelcase(configKey, { locale: 'en-US' }), npmConfig.get(configKey)]) as any, // eslint-disable-line
), configFromCliOpts) as unknown as ConfigWithDeprecatedSettings
// Resolving the current working directory to its actual location is crucial.
// This prevents potential inconsistencies in the future, especially when processing or mapping subdirectories.
const cwd = fs.realpathSync(betterPathResolve(cliOptions.dir ?? npmConfig.localPrefix))
@@ -499,9 +502,9 @@ export async function getConfig (opts: {
const workspaceManifest = await readWorkspaceManifest(pnpmConfig.workspaceDir)
pnpmConfig.workspacePackagePatterns = cliOptions['workspace-packages'] as string[] ?? workspaceManifest?.packages ?? ['.']
pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
if (workspaceManifest) {
Object.assign(pnpmConfig, getOptionsFromPnpmSettings(pnpmConfig.workspaceDir, workspaceManifest, pnpmConfig.rootProjectManifest))
Object.assign(pnpmConfig, getOptionsFromPnpmSettings(pnpmConfig.workspaceDir, workspaceManifest, pnpmConfig.rootProjectManifest), configFromCliOpts)
pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
}
}
}

View File

@@ -118,6 +118,7 @@ export async function handler (opts: PatchCommitCommandOptions, params: string[]
return install.handler({
...opts,
patchedDependencies: rootProjectManifest!.pnpm!.patchedDependencies!,
rootProjectManifest,
rawLocalConfig: {
...opts.rawLocalConfig,

View File

@@ -281,6 +281,7 @@ export type InstallCommandOptions = Pick<Config,
| 'lockfileOnly'
| 'modulesDir'
| 'nodeLinker'
| 'patchedDependencies'
| 'pnpmfile'
| 'preferFrozenLockfile'
| 'preferWorkspacePackages'

View File

@@ -79,6 +79,7 @@ test('patch from configuration dependency is applied', async () => {
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
patchedDependencies: rootProjectManifest.pnpm?.patchedDependencies,
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
}, ['@pnpm.e2e/foo@100.0.0'])

View File

@@ -21,7 +21,7 @@ beforeEach(() => {
const f = fixtures(__dirname)
function addPatch (key: string, patchFixture: string, patchDest: string): void {
function addPatch (key: string, patchFixture: string, patchDest: string): ProjectManifest {
fs.mkdirSync(path.dirname(patchDest), { recursive: true })
fs.copyFileSync(patchFixture, patchDest)
let manifestText = fs.readFileSync('package.json', 'utf-8')
@@ -35,6 +35,7 @@ function addPatch (key: string, patchFixture: string, patchDest: string): void {
}
manifestText = JSON.stringify(manifest, undefined, 2) + '\n'
fs.writeFileSync('package.json', manifestText)
return manifest
}
const unpatchedModulesDir = (v: 1 | 2 | 3) => `node_modules/.pnpm/@pnpm.e2e+console-log@${v}.0.0/node_modules`
@@ -54,12 +55,13 @@ test('bare package name as a patchedDependencies key should apply to all version
}, ['@pnpm.e2e/depends-on-console-log@1.0.0'])
fs.rmSync('pnpm-lock.yaml')
addPatch('@pnpm.e2e/console-log', patchFixture, 'patches/console-log.patch')
const rootProjectManifest = addPatch('@pnpm.e2e/console-log', patchFixture, 'patches/console-log.patch')
await install.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
frozenLockfile: false,
patchedDependencies: rootProjectManifest.pnpm?.patchedDependencies,
})
{
@@ -105,12 +107,13 @@ test('bare package name as a patchedDependencies key should apply to all possibl
}, ['@pnpm.e2e/depends-on-console-log@1.0.0'])
fs.rmSync('pnpm-lock.yaml')
addPatch('@pnpm.e2e/console-log', patchFixture, 'patches/console-log.patch')
const rootProjectManifest = addPatch('@pnpm.e2e/console-log', patchFixture, 'patches/console-log.patch')
await install.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
frozenLockfile: false,
patchedDependencies: rootProjectManifest.pnpm?.patchedDependencies,
})
// the common patch does not apply to v1
@@ -153,12 +156,13 @@ test('package name with version is prioritized over bare package name as keys of
fs.rmSync('pnpm-lock.yaml')
addPatch('@pnpm.e2e/console-log', commonPatchFixture, 'patches/console-log.patch')
addPatch('@pnpm.e2e/console-log@2.0.0', specializedPatchFixture, 'patches/console-log@2.0.0.patch')
const rootProjectManifest = addPatch('@pnpm.e2e/console-log@2.0.0', specializedPatchFixture, 'patches/console-log@2.0.0.patch')
await install.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
frozenLockfile: false,
patchedDependencies: rootProjectManifest.pnpm?.patchedDependencies,
})
// the common patch applies to v1
@@ -209,12 +213,13 @@ test('package name with version as a patchedDependencies key does not affect oth
fs.rmSync('pnpm-lock.yaml')
addPatch('@pnpm.e2e/console-log@2.0.0', patchFixture2, 'patches/console-log@2.0.0.patch')
addPatch('@pnpm.e2e/console-log@3.0.0', patchFixture3, 'patches/console-log@3.0.0.patch')
const rootProjectManifest = addPatch('@pnpm.e2e/console-log@3.0.0', patchFixture3, 'patches/console-log@3.0.0.patch')
await install.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
frozenLockfile: false,
patchedDependencies: rootProjectManifest.pnpm?.patchedDependencies,
})
// v1 remains unpatched
@@ -254,12 +259,13 @@ test('failure to apply patch with package name and version would cause throw an
}, ['@pnpm.e2e/depends-on-console-log@1.0.0'])
fs.rmSync('pnpm-lock.yaml')
addPatch('@pnpm.e2e/console-log@1.0.0', patchFixture, 'patches/console-log@1.0.0.patch')
const rootProjectManifest = addPatch('@pnpm.e2e/console-log@1.0.0', patchFixture, 'patches/console-log@1.0.0.patch')
const promise = install.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
frozenLockfile: false,
patchedDependencies: rootProjectManifest.pnpm?.patchedDependencies,
})
await expect(promise).rejects.toHaveProperty(['message'], expect.stringContaining('Could not apply patch'))
await expect(promise).rejects.toHaveProperty(['message'], expect.stringContaining(path.resolve('patches/console-log@1.0.0.patch')))

11
pnpm/test/config.ts Normal file
View File

@@ -0,0 +1,11 @@
import fs from 'fs'
import { WANTED_LOCKFILE } from '@pnpm/constants'
import { prepare } from '@pnpm/prepare'
import { execPnpmSync } from './utils'
test('read settings from pnpm-workspace.yaml', async () => {
prepare()
fs.writeFileSync('pnpm-workspace.yaml', 'useLockfile: false', 'utf8')
expect(execPnpmSync(['install']).status).toBe(0)
expect(fs.existsSync(WANTED_LOCKFILE)).toBeFalsy()
})

View File

@@ -768,15 +768,16 @@ test('deploy with a shared lockfile should correctly handle packageExtensions',
})
test('deploy with a shared lockfile should correctly handle patchedDependencies', async () => {
const patchedDependencies = {
'is-positive': '__patches__/is-positive.patch',
}
const preparedManifests: Record<string, ProjectManifest> = {
root: {
name: 'root',
version: '0.0.0',
private: true,
pnpm: {
patchedDependencies: {
'is-positive': '__patches__/is-positive.patch',
},
patchedDependencies,
},
},
'project-0': {
@@ -818,6 +819,7 @@ test('deploy with a shared lockfile should correctly handle patchedDependencies'
allProjectsGraph,
selectedProjectsGraph: allProjectsGraph,
dir: process.cwd(),
patchedDependencies,
recursive: true,
lockfileDir: process.cwd(),
workspaceDir: process.cwd(),
@@ -828,6 +830,7 @@ test('deploy with a shared lockfile should correctly handle patchedDependencies'
...DEFAULT_OPTS,
allProjects,
dir: process.cwd(),
patchedDependencies,
recursive: true,
selectedProjectsGraph,
sharedWorkspaceLockfile: true,

View File

@@ -275,12 +275,14 @@ test('pnpm licenses should work with file protocol dependency', async () => {
test('pnpm licenses should work with git protocol dep that have patches', async () => {
const workspaceDir = tempDir()
f.copy('with-git-protocol-patched-deps', workspaceDir)
const patchedDependencies = JSON.parse(fs.readFileSync(path.join(workspaceDir, 'package.json'), 'utf8')).pnpm.patchedDependencies
const storeDir = path.join(workspaceDir, 'store')
await install.handler({
...DEFAULT_OPTS,
dir: workspaceDir,
frozenLockfile: true,
patchedDependencies,
pnpmHomeDir: '',
storeDir,
})