feat: reading settings from pnpm-workspace.yaml (#9121)

Related discussion: https://github.com/orgs/pnpm/discussions/9037

close #9033
This commit is contained in:
Zoltan Kochan
2025-02-22 02:10:43 +01:00
committed by GitHub
parent 428915cd9d
commit 8fcc221394
54 changed files with 401 additions and 122 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/workspace.manifest-writer": major
---
Initial release.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-installation": major
---
Read `onlyBuiltDependencies` and `ignoredBuiltDependencies` from options.

View File

@@ -0,0 +1,6 @@
---
"@pnpm/config": minor
"pnpm": minor
---
Allow to set the "pnpm" settings from `package.json` via the `pnpm-workspace.yaml` file.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-rebuild": major
---
Don't rebuild packages that are not in the approved list.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/exec.build-commands": major
---
Read `onlyBuiltDependencies` and `ignoredBuiltDependencies` from `options`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/core": major
---
By default, don't allow to run scripts of dependencies.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/types": minor
---
Export PnpmSettings.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/deps.status": major
---
Read `configDependencies` from `options`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/workspace.read-manifest": minor
---
Extend WorkspaceManifest with PnpmSettings.

View File

@@ -0,0 +1,6 @@
---
"@pnpm/workspace.read-manifest": minor
"pnpm": minor
---
The `packages` field in `pnpm-workspace.yaml` became optional.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/workspace.manifest-writer": major
---
Initial release.

View File

@@ -0,0 +1,2 @@
packages:
- pkg

View File

@@ -0,0 +1,2 @@
packages:
- "*"

View File

@@ -7,6 +7,7 @@ import {
type SslConfig,
} from '@pnpm/types'
import type { Hooks } from '@pnpm/pnpmfile'
import { type OptionsFromRootManifest } from './getOptionsFromRootManifest'
export type UniversalOptions = Pick<Config, 'color' | 'dir' | 'rawConfig' | 'rawLocalConfig'>
@@ -17,7 +18,7 @@ export interface WantedPackageManager {
export type VerifyDepsBeforeRun = 'install' | 'warn' | 'error' | 'prompt' | false
export interface Config {
export interface Config extends OptionsFromRootManifest {
allProjects?: Project[]
selectedProjectsGraph?: ProjectsGraph
allProjectsGraph?: ProjectsGraph

View File

@@ -6,10 +6,12 @@ import {
type PackageExtension,
type PeerDependencyRules,
type ProjectManifest,
type PnpmSettings,
} from '@pnpm/types'
import mapValues from 'ramda/src/map'
import pick from 'ramda/src/pick'
export interface OptionsFromRootManifest {
export type OptionsFromRootManifest = {
allowedDeprecatedVersions?: AllowedDeprecatedVersions
allowNonAppliedPatches?: boolean
overrides?: Record<string, string>
@@ -22,63 +24,50 @@ export interface OptionsFromRootManifest {
patchedDependencies?: Record<string, string>
peerDependencyRules?: PeerDependencyRules
supportedArchitectures?: SupportedArchitectures
}
} & Pick<PnpmSettings, 'configDependencies'>
export function getOptionsFromRootManifest (manifestDir: string, manifest: ProjectManifest): OptionsFromRootManifest {
// 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
const overrides = mapValues(
createVersionReferencesReplacer(manifest),
{
const settings: OptionsFromRootManifest = getOptionsFromPnpmSettings(manifestDir, {
...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
overrides: {
...manifest.resolutions,
...manifest.pnpm?.overrides,
}
)
const neverBuiltDependencies = manifest.pnpm?.neverBuiltDependencies
let onlyBuiltDependencies = manifest.pnpm?.onlyBuiltDependencies
const onlyBuiltDependenciesFile = manifest.pnpm?.onlyBuiltDependenciesFile
if (onlyBuiltDependenciesFile == null && neverBuiltDependencies == null && onlyBuiltDependencies == null) {
onlyBuiltDependencies = []
},
}, manifest)
return settings
}
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)
if (settings.overrides && manifest) {
settings.overrides = mapValues(createVersionReferencesReplacer(manifest), settings.overrides)
}
const packageExtensions = manifest.pnpm?.packageExtensions
const ignoredOptionalDependencies = manifest.pnpm?.ignoredOptionalDependencies
const peerDependencyRules = manifest.pnpm?.peerDependencyRules
const allowedDeprecatedVersions = manifest.pnpm?.allowedDeprecatedVersions
const allowNonAppliedPatches = manifest.pnpm?.allowNonAppliedPatches
let patchedDependencies = manifest.pnpm?.patchedDependencies
if (patchedDependencies) {
patchedDependencies = { ...patchedDependencies }
for (const [dep, patchFile] of Object.entries(patchedDependencies)) {
if (pnpmSettings.onlyBuiltDependenciesFile) {
settings.onlyBuiltDependenciesFile = path.join(manifestDir, pnpmSettings.onlyBuiltDependenciesFile)
}
if (pnpmSettings.patchedDependencies) {
settings.patchedDependencies = { ...pnpmSettings.patchedDependencies }
for (const [dep, patchFile] of Object.entries(pnpmSettings.patchedDependencies)) {
if (path.isAbsolute(patchFile)) continue
patchedDependencies[dep] = path.join(manifestDir, patchFile)
settings.patchedDependencies[dep] = path.join(manifestDir, patchFile)
}
}
const supportedArchitectures = {
os: manifest.pnpm?.supportedArchitectures?.os ?? ['current'],
cpu: manifest.pnpm?.supportedArchitectures?.cpu ?? ['current'],
libc: manifest.pnpm?.supportedArchitectures?.libc ?? ['current'],
}
const settings: OptionsFromRootManifest = {
allowedDeprecatedVersions,
allowNonAppliedPatches,
overrides,
neverBuiltDependencies,
packageExtensions,
ignoredOptionalDependencies,
peerDependencyRules,
patchedDependencies,
supportedArchitectures,
ignoredBuiltDependencies: manifest.pnpm?.ignoredBuiltDependencies,
}
if (onlyBuiltDependencies) {
settings.onlyBuiltDependencies = onlyBuiltDependencies
}
if (onlyBuiltDependenciesFile) {
settings.onlyBuiltDependenciesFile = path.join(manifestDir, onlyBuiltDependenciesFile)
}
return settings
}

View File

@@ -31,9 +31,10 @@ import { getWorkspaceConcurrency } from './concurrency'
import { readWorkspaceManifest } from '@pnpm/workspace.read-manifest'
import { types } from './types'
import { getOptionsFromPnpmSettings, getOptionsFromRootManifest } from './getOptionsFromRootManifest'
export { types }
export { getOptionsFromRootManifest, type OptionsFromRootManifest } from './getOptionsFromRootManifest'
export { getOptionsFromRootManifest, getOptionsFromPnpmSettings, type OptionsFromRootManifest } from './getOptionsFromRootManifest'
export * from './readLocalConfig'
export type { Config, UniversalOptions, WantedPackageManager, VerifyDepsBeforeRun }
@@ -484,13 +485,19 @@ export async function getConfig (opts: {
if (pnpmConfig.rootProjectManifest.packageManager) {
pnpmConfig.wantedPackageManager = parsePackageManager(pnpmConfig.rootProjectManifest.packageManager)
}
if (pnpmConfig.rootProjectManifest) {
Object.assign(pnpmConfig, getOptionsFromRootManifest(pnpmConfig.rootProjectManifestDir, pnpmConfig.rootProjectManifest))
}
}
if (pnpmConfig.workspaceDir != null) {
const workspaceManifest = await readWorkspaceManifest(pnpmConfig.workspaceDir)
pnpmConfig.workspacePackagePatterns = cliOptions['workspace-packages'] as string[] ?? workspaceManifest?.packages
pnpmConfig.workspacePackagePatterns = cliOptions['workspace-packages'] as string[] ?? workspaceManifest?.packages ?? ['.']
pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
if (workspaceManifest) {
Object.assign(pnpmConfig, getOptionsFromPnpmSettings(pnpmConfig.workspaceDir, workspaceManifest, pnpmConfig.rootProjectManifest))
}
}
pnpmConfig.failedToLoadBuiltInConfig = failedToLoadBuiltInConfig

View File

@@ -0,0 +1,4 @@
packages: []
onlyBuiltDependencies:
- foo

View File

@@ -80,7 +80,7 @@ test('getOptionsFromRootManifest() throws an error if cannot resolve an override
test('getOptionsFromRootManifest() should return an empty onlyBuiltDependencies list by default', () => {
const options = getOptionsFromRootManifest(process.cwd(), {})
expect(options.onlyBuiltDependencies).toStrictEqual([])
expect(options.onlyBuiltDependencies).toStrictEqual(undefined)
})
test('getOptionsFromRootManifest() should return the list fromm onlyBuiltDependencies', () => {

View File

@@ -1009,3 +1009,18 @@ test('xxx', async () => {
process.env = oldEnv
})
test('settings from pnpm-workspace.yaml are read', async () => {
const workspaceDir = f.find('settings-in-workspace-yaml')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {},
workspaceDir,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.onlyBuiltDependencies).toStrictEqual(['foo'])
})

View File

@@ -59,6 +59,7 @@ export type CheckDepsStatusOptions = Pick<Config,
| 'workspaceDir'
| 'patchesDir'
| 'pnpmfile'
| 'configDependencies'
> & {
ignoreFilteredInstallCache?: boolean
ignoredWorkspaceStateSettings?: Array<keyof WorkspaceStateSettings>
@@ -136,7 +137,7 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions, workspaceState: W
}
}
}
if ((rootProjectManifest?.pnpm?.configDependencies != null || workspaceState.configDependencies != null) && !equals(rootProjectManifest?.pnpm?.configDependencies ?? {}, workspaceState.configDependencies ?? {})) {
if ((opts.configDependencies != null || workspaceState.configDependencies != null) && !equals(opts.configDependencies ?? {}, workspaceState.configDependencies ?? {})) {
return {
upToDate: false,
issue: 'Configuration dependencies are not up to date',

View File

@@ -30,7 +30,6 @@
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/exec/build-commands#readme",
"devDependencies": {
"@pnpm/config": "workspace:*",
"@pnpm/exec.build-commands": "workspace:*",
"@pnpm/plugin-commands-installation": "workspace:*",
"@pnpm/prepare": "workspace:*",
@@ -47,6 +46,7 @@
"@pnpm/prepare-temp-dir": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"@pnpm/util.lex-comparator": "catalog:",
"@pnpm/workspace.manifest-writer": "workspace:*",
"chalk": "catalog:",
"enquirer": "catalog:",
"render-help": "catalog:"

View File

@@ -6,9 +6,10 @@ import renderHelp from 'render-help'
import { prompt } from 'enquirer'
import chalk from 'chalk'
import { rebuild, type RebuildCommandOpts } from '@pnpm/plugin-commands-rebuild'
import { updateWorkspaceManifest } from '@pnpm/workspace.manifest-writer'
import { getAutomaticallyIgnoredBuilds } from './getAutomaticallyIgnoredBuilds'
export type ApproveBuildsCommandOpts = Pick<Config, 'modulesDir' | 'dir' | 'rootProjectManifest' | 'rootProjectManifestDir'>
export type ApproveBuildsCommandOpts = Pick<Config, 'modulesDir' | 'dir' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'onlyBuiltDependencies' | 'ignoredBuiltDependencies'>
export const commandNames = ['approve-builds']
@@ -87,28 +88,24 @@ export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOp
} as any) as any // eslint-disable-line @typescript-eslint/no-explicit-any
const buildPackages = result.map(({ value }: { value: string }) => value)
const ignoredPackages = automaticallyIgnoredBuilds.filter((automaticallyIgnoredBuild) => !buildPackages.includes(automaticallyIgnoredBuild))
let updatedIgnoredBuiltDependencies: string[] | undefined
if (ignoredPackages.length) {
if (opts.rootProjectManifest.pnpm?.ignoredBuiltDependencies == null) {
opts.rootProjectManifest.pnpm = {
...opts.rootProjectManifest.pnpm,
ignoredBuiltDependencies: sortUniqueStrings(ignoredPackages),
}
if (opts.ignoredBuiltDependencies == null) {
updatedIgnoredBuiltDependencies = sortUniqueStrings(ignoredPackages)
} else {
opts.rootProjectManifest.pnpm.ignoredBuiltDependencies = sortUniqueStrings([
...opts.rootProjectManifest.pnpm.ignoredBuiltDependencies,
updatedIgnoredBuiltDependencies = sortUniqueStrings([
...opts.ignoredBuiltDependencies,
...ignoredPackages,
])
}
}
let updatedOnlyBuiltDependencies: string[] | undefined
if (buildPackages.length) {
if (opts.rootProjectManifest.pnpm?.onlyBuiltDependencies == null) {
opts.rootProjectManifest.pnpm = {
...opts.rootProjectManifest.pnpm,
onlyBuiltDependencies: sortUniqueStrings(buildPackages),
}
if (opts.onlyBuiltDependencies == null) {
updatedOnlyBuiltDependencies = sortUniqueStrings(buildPackages)
} else {
opts.rootProjectManifest.pnpm.onlyBuiltDependencies = sortUniqueStrings([
...opts.rootProjectManifest.pnpm.onlyBuiltDependencies,
updatedOnlyBuiltDependencies = sortUniqueStrings([
...opts.onlyBuiltDependencies,
...buildPackages,
])
}
@@ -126,9 +123,26 @@ Do you approve?`,
}
}
const { writeProjectManifest } = await readProjectManifest(opts.rootProjectManifestDir)
await writeProjectManifest(opts.rootProjectManifest)
if (opts.rootProjectManifest.pnpm?.ignoredBuiltDependencies != null || opts.rootProjectManifest.pnpm?.onlyBuiltDependencies != null || opts.workspaceDir == null) {
opts.rootProjectManifest.pnpm = opts.rootProjectManifest.pnpm ?? {}
if (updatedOnlyBuiltDependencies) {
opts.rootProjectManifest.pnpm.onlyBuiltDependencies = updatedOnlyBuiltDependencies
}
if (updatedIgnoredBuiltDependencies) {
opts.rootProjectManifest.pnpm.ignoredBuiltDependencies = updatedIgnoredBuiltDependencies
}
await writeProjectManifest(opts.rootProjectManifest)
} else {
await updateWorkspaceManifest(opts.workspaceDir, {
onlyBuiltDependencies: updatedOnlyBuiltDependencies,
ignoredBuiltDependencies: updatedIgnoredBuiltDependencies,
})
}
if (buildPackages.length) {
return rebuild.handler(opts, buildPackages)
return rebuild.handler({
...opts,
onlyBuiltDependencies: updatedOnlyBuiltDependencies,
}, buildPackages)
}
}

View File

@@ -30,6 +30,9 @@
{
"path": "../../pkg-manifest/read-project-manifest"
},
{
"path": "../../workspace/manifest-writer"
},
{
"path": "../plugin-commands-rebuild"
}

View File

@@ -51,7 +51,7 @@ export type StrictRebuildOptions = {
peersSuffixMaxLength: number
strictStorePkgContentCheck: boolean
fetchFullMetadata?: boolean
} & Pick<Config, 'sslConfigs'>
} & Pick<Config, 'sslConfigs' | 'onlyBuiltDependencies' | 'onlyBuiltDependenciesFile' | 'neverBuiltDependencies'>
export type RebuildOptions = Partial<StrictRebuildOptions> &
Pick<StrictRebuildOptions, 'storeDir' | 'storeController'> & Pick<Config, 'rootProjectManifest' | 'rootProjectManifestDir'>
@@ -106,5 +106,8 @@ export async function extendRebuildOptions (
...(opts.rootProjectManifest ? getOptionsFromRootManifest(opts.rootProjectManifestDir, opts.rootProjectManifest) : {}),
}
extendedOpts.registries = normalizeRegistries(extendedOpts.registries)
if (extendedOpts.neverBuiltDependencies == null && extendedOpts.onlyBuiltDependencies == null && extendedOpts.onlyBuiltDependenciesFile == null) {
extendedOpts.onlyBuiltDependencies = []
}
return extendedOpts
}

View File

@@ -28,6 +28,7 @@ export const DEFAULT_OPTS = {
localAddress: undefined,
lock: false,
lockStaleDuration: 90,
neverBuiltDependencies: [],
networkConcurrency: 16,
offline: false,
pending: false,

View File

@@ -726,7 +726,9 @@ test('`pnpm run -r` should avoid infinite recursion', async () => {
},
},
])
writeYamlFile('pnpm-workspace.yaml', {})
writeYamlFile('pnpm-workspace.yaml', {
packages: ['**'],
})
await execa(pnpmBin, [
'install',

View File

@@ -0,0 +1,2 @@
packages:
- packages/**

View File

@@ -127,33 +127,35 @@ export interface PeerDependencyRules {
export type AllowedDeprecatedVersions = Record<string, string>
export interface PnpmSettings {
configDependencies?: Record<string, string>
neverBuiltDependencies?: string[]
onlyBuiltDependencies?: string[]
onlyBuiltDependenciesFile?: string
ignoredBuiltDependencies?: string[]
overrides?: Record<string, string>
packageExtensions?: Record<string, PackageExtension>
ignoredOptionalDependencies?: string[]
peerDependencyRules?: PeerDependencyRules
allowedDeprecatedVersions?: AllowedDeprecatedVersions
allowNonAppliedPatches?: boolean
patchedDependencies?: Record<string, string>
updateConfig?: {
ignoreDependencies?: string[]
}
auditConfig?: {
ignoreCves?: string[]
ignoreGhsas?: string[]
}
requiredScripts?: string[]
supportedArchitectures?: SupportedArchitectures
executionEnv?: ExecutionEnv
}
export interface ProjectManifest extends BaseManifest {
packageManager?: string
workspaces?: string[]
pnpm?: {
configDependencies?: Record<string, string>
neverBuiltDependencies?: string[]
onlyBuiltDependencies?: string[]
onlyBuiltDependenciesFile?: string
ignoredBuiltDependencies?: string[]
overrides?: Record<string, string>
packageExtensions?: Record<string, PackageExtension>
ignoredOptionalDependencies?: string[]
peerDependencyRules?: PeerDependencyRules
allowedDeprecatedVersions?: AllowedDeprecatedVersions
allowNonAppliedPatches?: boolean
patchedDependencies?: Record<string, string>
updateConfig?: {
ignoreDependencies?: string[]
}
auditConfig?: {
ignoreCves?: string[]
ignoreGhsas?: string[]
}
requiredScripts?: string[]
supportedArchitectures?: SupportedArchitectures
executionEnv?: ExecutionEnv
}
pnpm?: PnpmSettings
private?: boolean
resolutions?: Record<string, string>
}

View File

@@ -268,6 +268,9 @@ export function extendOptions (
}
}
}
if (opts.neverBuiltDependencies == null && opts.onlyBuiltDependencies == null && opts.onlyBuiltDependenciesFile == null) {
opts.onlyBuiltDependencies = []
}
if (opts.onlyBuiltDependencies && opts.neverBuiltDependencies) {
throw new PnpmError('CONFIG_CONFLICT_BUILT_DEPENDENCIES', 'Cannot have both neverBuiltDependencies and onlyBuiltDependencies')
}

View File

@@ -473,9 +473,10 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependencies',
prepareEmpty()
const reporter = sinon.spy()
const onlyBuiltDependencies = ['@pnpm.e2e/install-script-example']
const neverBuiltDependencies: string[] | undefined = undefined
const { updatedManifest: manifest } = await addDependenciesToPackage({},
['@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0', '@pnpm.e2e/install-script-example'],
testDefaults({ fastUnpack: false, onlyBuiltDependencies, reporter })
testDefaults({ fastUnpack: false, onlyBuiltDependencies, neverBuiltDependencies, reporter })
)
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeFalsy()
@@ -494,6 +495,7 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependencies',
fastUnpack: false,
frozenLockfile: true,
ignoredBuiltDependencies: ['@pnpm.e2e/pre-and-postinstall-scripts-example'],
neverBuiltDependencies,
onlyBuiltDependencies,
reporter,
}))

View File

@@ -23,6 +23,7 @@ test('patch package', async () => {
'is-positive@1.0.0': patchPath,
}
const opts = testDefaults({
neverBuiltDependencies: undefined,
onlyBuiltDependencies: [],
fastUnpack: false,
sideEffectsCacheRead: true,
@@ -288,6 +289,7 @@ test('patch package when the package is not in onlyBuiltDependencies list', asyn
sideEffectsCacheRead: true,
sideEffectsCacheWrite: true,
patchedDependencies,
neverBuiltDependencies: undefined,
onlyBuiltDependencies: [],
}, {}, {}, { packageImportMethod: 'hardlink' })
await install({
@@ -355,6 +357,7 @@ test('patch package when the package is not in onlyBuiltDependencies list', asyn
fastUnpack: false,
sideEffectsCacheRead: true,
sideEffectsCacheWrite: true,
neverBuiltDependencies: undefined,
onlyBuiltDependencies: [],
offline: true,
}, {}, {}, { packageImportMethod: 'hardlink' }))

View File

@@ -56,6 +56,7 @@ export function testDefaults<T> (
)
const result = {
cacheDir,
neverBuiltDependencies: [] as string[],
registries: {
default: registry,
},

View File

@@ -0,0 +1,2 @@
packages:
- "*"

View File

@@ -0,0 +1,2 @@
packages:
- "*"

View File

@@ -191,7 +191,9 @@ export async function handler (
!opts.recursive &&
opts.workspaceDir === opts.dir &&
!opts.ignoreWorkspaceRootCheck &&
!opts.workspaceRoot
!opts.workspaceRoot &&
opts.workspacePackagePatterns &&
opts.workspacePackagePatterns.length > 1
) {
throw new PnpmError('ADDING_TO_ROOT',
'Running this command will add the dependency to the workspace root, ' +

View File

@@ -259,6 +259,7 @@ export type InstallCommandOptions = Pick<Config,
| 'bin'
| 'catalogs'
| 'cliOptions'
| 'configDependencies'
| 'dedupeInjectedDeps'
| 'dedupeDirectDeps'
| 'dedupePeerDependents'

View File

@@ -102,6 +102,7 @@ export type InstallDepsOptions = Pick<Config,
| 'extraEnv'
| 'ignoreWorkspaceCycles'
| 'disallowWorkspaceCycles'
| 'configDependencies'
> & CreateStoreControllerOptions & {
argv: {
original: string[]
@@ -171,8 +172,8 @@ when running add/update with the --workspace option')
opts['preserveWorkspaceProtocol'] = !opts.linkWorkspacePackages
}
let store = await createOrConnectStoreController(opts)
if (opts.rootProjectManifest?.pnpm?.configDependencies) {
await installConfigDeps(opts.rootProjectManifest.pnpm.configDependencies, {
if (opts.configDependencies) {
await installConfigDeps(opts.configDependencies, {
registries: opts.registries,
rootDir: opts.lockfileDir ?? opts.rootProjectManifestDir,
store: store.ctrl,
@@ -346,7 +347,7 @@ when running add/update with the --workspace option')
workspaceDir: opts.workspaceDir ?? opts.lockfileDir ?? opts.dir,
pnpmfileExists: opts.hooks?.calculatePnpmfileChecksum != null,
filteredInstall: allProjects.length !== Object.keys(opts.selectedProjectsGraph ?? {}).length,
configDependencies: opts.rootProjectManifest?.pnpm?.configDependencies,
configDependencies: opts.configDependencies,
})
}
if (opts.strictDepBuilds && ignoredBuilds?.length) {
@@ -406,7 +407,7 @@ when running add/update with the --workspace option')
workspaceDir: opts.workspaceDir ?? opts.lockfileDir ?? opts.dir,
pnpmfileExists: opts.hooks?.calculatePnpmfileChecksum != null,
filteredInstall: allProjects.length !== Object.keys(opts.selectedProjectsGraph ?? {}).length,
configDependencies: opts.rootProjectManifest?.pnpm?.configDependencies,
configDependencies: opts.configDependencies,
})
}
}
@@ -432,7 +433,7 @@ async function recursiveInstallThenUpdateWorkspaceState (
workspaceDir: opts.workspaceDir,
pnpmfileExists: opts.hooks?.calculatePnpmfileChecksum != null,
filteredInstall: allProjects.length !== Object.keys(opts.selectedProjectsGraph ?? {}).length,
configDependencies: opts.rootProjectManifest?.pnpm?.configDependencies,
configDependencies: opts.configDependencies,
})
}
return recursiveResult

View File

@@ -46,6 +46,7 @@ import { IgnoredBuildsError } from './errors'
export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
| 'bail'
| 'configDependencies'
| 'dedupePeerDependents'
| 'depth'
| 'globalPnpmfile'

View File

@@ -131,6 +131,7 @@ export async function handler (
| 'allProjectsGraph'
| 'bail'
| 'bin'
| 'configDependencies'
| 'dev'
| 'engineStrict'
| 'globalPnpmfile'
@@ -164,8 +165,8 @@ export async function handler (
optionalDependencies: opts.optional !== false,
}
let store = await createOrConnectStoreController(opts)
if (opts.rootProjectManifest?.pnpm?.configDependencies) {
await installConfigDeps(opts.rootProjectManifest.pnpm.configDependencies, {
if (opts.configDependencies) {
await installConfigDeps(opts.configDependencies, {
registries: opts.registries,
rootDir: opts.lockfileDir ?? opts.rootProjectManifestDir,
store: store.ctrl,

View File

@@ -19,6 +19,7 @@ test('configuration dependency is installed', async () => {
await install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
@@ -35,6 +36,7 @@ test('configuration dependency is installed', async () => {
await install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
@@ -51,6 +53,7 @@ test('configuration dependency is installed', async () => {
await install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
@@ -74,6 +77,7 @@ test('patch from configuration dependency is applied', async () => {
await add.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
@@ -94,6 +98,7 @@ test('installation fails if the checksum of the config dependency is invalid', a
await expect(install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
@@ -112,6 +117,7 @@ test('installation fails if the config dependency does not have a checksum', asy
await expect(install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
@@ -131,6 +137,7 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependenciesFil
await add.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
@@ -144,6 +151,7 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependenciesFil
await install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
frozenLockfile: true,
rootProjectManifest,
@@ -169,6 +177,7 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependenciesFil
await add.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
@@ -182,6 +191,7 @@ test('selectively allow scripts in some dependencies by onlyBuiltDependenciesFil
await install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
frozenLockfile: true,
rootProjectManifest,

19
pnpm-lock.yaml generated
View File

@@ -2099,6 +2099,9 @@ importers:
'@pnpm/util.lex-comparator':
specifier: 'catalog:'
version: 3.0.0
'@pnpm/workspace.manifest-writer':
specifier: workspace:*
version: link:../../workspace/manifest-writer
chalk:
specifier: 'catalog:'
version: 4.1.2
@@ -8006,6 +8009,19 @@ importers:
specifier: workspace:*
version: 'link:'
workspace/manifest-writer:
dependencies:
'@pnpm/workspace.read-manifest':
specifier: workspace:*
version: link:../read-manifest
write-yaml-file:
specifier: 'catalog:'
version: 5.0.0
devDependencies:
'@pnpm/workspace.manifest-writer':
specifier: workspace:*
version: 'link:'
workspace/pkgs-graph:
dependencies:
'@pnpm/npm-package-arg':
@@ -8042,6 +8058,9 @@ importers:
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
read-yaml-file:
specifier: 'catalog:'
version: 2.1.0

View File

@@ -0,0 +1,2 @@
packages:
- "**"

View File

@@ -240,7 +240,8 @@ test('`pnpm -r add` should fail if no package name was provided', () => {
},
])
fs.writeFileSync('pnpm-workspace.yaml', '', 'utf8')
fs.writeFileSync('pnpm-workspace.yaml', `packages:
- project`, 'utf8')
const { status, stdout } = execPnpmSync(['-r', 'add'])
@@ -303,7 +304,8 @@ test('recursive install should fail if the used pnpm version does not satisfy th
},
])
fs.writeFileSync('pnpm-workspace.yaml', '', 'utf8')
fs.writeFileSync('pnpm-workspace.yaml', `packages:
- "*"`, 'utf8')
process.chdir('project-1')
@@ -336,7 +338,8 @@ test('engine-strict=true: recursive install should fail if the used Node version
},
])
fs.writeFileSync('pnpm-workspace.yaml', '', 'utf8')
fs.writeFileSync('pnpm-workspace.yaml', `packages:
- "*"`, 'utf8')
process.chdir('project-1')
@@ -369,7 +372,8 @@ test('engine-strict=false: recursive install should not fail if the used Node ve
},
])
fs.writeFileSync('pnpm-workspace.yaml', '', 'utf8')
fs.writeFileSync('pnpm-workspace.yaml', `packages:
- "*"`, 'utf8')
process.chdir('project-1')

View File

@@ -1,7 +1,7 @@
import fs from 'fs'
import path from 'path'
import { STORE_VERSION } from '@pnpm/constants'
import { prepare, preparePackages } from '@pnpm/prepare'
import { preparePackages } from '@pnpm/prepare'
import { type LockfileFile } from '@pnpm/lockfile.types'
import { sync as readYamlFile } from 'read-yaml-file'
import { isCI } from 'ci-info'
@@ -77,7 +77,8 @@ test('workspace .npmrc is always read', async () => {
])
const storeDir = path.resolve('../store')
fs.writeFileSync('pnpm-workspace.yaml', '', 'utf8')
fs.writeFileSync('pnpm-workspace.yaml', `packages:
- workspace/*`, 'utf8')
fs.writeFileSync('.npmrc', 'shamefully-flatten = true\nshared-workspace-lockfile=false', 'utf8')
fs.writeFileSync('workspace/project-2/.npmrc', 'hoist=false', 'utf8')
@@ -359,9 +360,22 @@ test('non-recursive install ignores filter from config', async () => {
})
test('adding new dependency in the root should fail if neither --workspace-root nor --ignore-workspace-root-check are used', async () => {
const project = prepare()
const project = preparePackages([
{
location: '.',
package: {
name: 'root',
},
},
{
name: 'project',
},
])['root']
fs.writeFileSync('pnpm-workspace.yaml', '', 'utf8')
fs.writeFileSync('pnpm-workspace.yaml', `packages:
- '.'
- 'project'
`, 'utf8')
{
const { status, stdout } = execPnpmSync(['add', 'is-positive'])
@@ -455,7 +469,8 @@ test('set recursive-install to false in .npmrc would disable recursive install i
])
process.chdir('workspace')
fs.writeFileSync('pnpm-workspace.yaml', '', 'utf8')
fs.writeFileSync('pnpm-workspace.yaml', `packages:
- "**"`, 'utf8')
fs.writeFileSync('.npmrc', `recursive-install = false
dedupe-peer-dependents = false`, 'utf8')
@@ -493,7 +508,8 @@ test('set recursive-install to false would install as --filter {.}...', async ()
])
process.chdir('workspace')
fs.writeFileSync('pnpm-workspace.yaml', '', 'utf8')
fs.writeFileSync('pnpm-workspace.yaml', `packages:
- "**"`, 'utf8')
fs.writeFileSync('.npmrc', 'recursive-install = false', 'utf8')
process.chdir('project-1')

View File

@@ -3,6 +3,14 @@ import { execPnpm } from '../utils'
test('`pnpm recursive rebuild` specific dependencies', async () => {
const projects = preparePackages([
{
location: '.',
package: {
pnpm: {
neverBuiltDependencies: [],
},
},
},
{
name: 'project-1',
version: '1.0.0',

View File

@@ -0,0 +1,13 @@
# @pnpm/workspace.manifest-writer
> Updates the workspace manifest file
## Install
```
pnpm add @pnpm/workspace.manifest-writer
```
## LICENSE
MIT

View File

@@ -0,0 +1,44 @@
{
"name": "@pnpm/workspace.manifest-writer",
"version": "1000.0.0-0",
"description": "Updates the workspace manifest file",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"!*.map"
],
"engines": {
"node": ">=18.12"
},
"scripts": {
"lint": "eslint \"src/**/*.ts\"",
"test": "pnpm run compile",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"repository": "https://github.com/pnpm/pnpm/blob/main/workspace/manifest-writer",
"keywords": [
"pnpm10",
"pnpm"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/workspace/manifest-writer#readme",
"dependencies": {
"@pnpm/workspace.read-manifest": "workspace:*",
"write-yaml-file": "catalog:"
},
"funding": "https://opencollective.com/pnpm",
"devDependencies": {
"@pnpm/workspace.manifest-writer": "workspace:*"
},
"exports": {
".": "./lib/index.js"
},
"jest": {
"preset": "@pnpm/jest-config"
}
}

View File

@@ -0,0 +1,10 @@
import { readWorkspaceManifest, type WorkspaceManifest } from '@pnpm/workspace.read-manifest'
import writeYamlFile from 'write-yaml-file'
export async function updateWorkspaceManifest (dir: string, updatedFields: Partial<WorkspaceManifest>): Promise<void> {
const manifest = await readWorkspaceManifest(dir)
await writeYamlFile(dir, {
...manifest,
updatedFields,
})
}

View File

@@ -0,0 +1,16 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../read-manifest"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../__typings__/**/*.d.ts"
]
}

View File

@@ -31,6 +31,7 @@
"dependencies": {
"@pnpm/constants": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/types": "workspace:*",
"read-yaml-file": "catalog:"
},
"funding": "https://opencollective.com/pnpm",

View File

@@ -1,5 +1,6 @@
import util from 'util'
import { WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
import { type PnpmSettings } from '@pnpm/types'
import path from 'node:path'
import readYamlFile from 'read-yaml-file'
import {
@@ -10,7 +11,7 @@ import {
} from './catalogs'
import { InvalidWorkspaceManifestError } from './errors/InvalidWorkspaceManifestError'
export interface WorkspaceManifest {
export interface WorkspaceManifest extends PnpmSettings {
packages: string[]
/**
@@ -75,7 +76,7 @@ function validateWorkspaceManifest (manifest: unknown): asserts manifest is Work
function assertValidWorkspaceManifestPackages (manifest: { packages?: unknown }): asserts manifest is { packages: string[] } {
if (!manifest.packages) {
throw new InvalidWorkspaceManifestError('packages field missing or empty')
return
}
if (!Array.isArray(manifest.packages)) {

View File

@@ -21,10 +21,10 @@ test('readWorkspaceManifest() throws on array content', async () => {
).rejects.toThrow('Expected object but found - array')
})
test('readWorkspaceManifest() throws on empty packages field', async () => {
test('readWorkspaceManifest() does not throw on empty packages field', async () => {
await expect(
readWorkspaceManifest(path.join(__dirname, '__fixtures__/packages-empty'))
).rejects.toThrow('packages field missing or empty')
).resolves.toBeTruthy()
})
test('readWorkspaceManifest() throws on string packages field', async () => {

View File

@@ -14,6 +14,9 @@
},
{
"path": "../../packages/error"
},
{
"path": "../../packages/types"
}
]
}