Files
pnpm/config/reader/src/getOptionsFromRootManifest.ts
Dami Oyeniyi 6d7903a8b7 fix: reject invalid overrides values (#11380)
* fix: reject invalid overrides values

* fix: improve overrides validation error messages
2026-05-05 01:06:33 +02:00

114 lines
4.4 KiB
TypeScript

import path from 'node:path'
import { envReplace } from '@pnpm/config.env-replace'
import { PnpmError } from '@pnpm/error'
import type {
AllowedDeprecatedVersions,
PackageExtension,
PeerDependencyRules,
PnpmSettings,
ProjectManifest,
SupportedArchitectures,
} from '@pnpm/types'
import { map as mapValues } from 'ramda'
export type OptionsFromRootManifest = {
allowedDeprecatedVersions?: AllowedDeprecatedVersions
allowUnusedPatches?: boolean
overrides?: Record<string, string>
packageExtensions?: Record<string, PackageExtension>
ignoredOptionalDependencies?: string[]
patchedDependencies?: Record<string, string>
peerDependencyRules?: PeerDependencyRules
supportedArchitectures?: SupportedArchitectures
allowBuilds?: Record<string, boolean | string>
requiredScripts?: string[]
} & Pick<PnpmSettings, 'configDependencies' | 'auditConfig' | 'agent' | 'updateConfig'>
export function getOptionsFromPnpmSettings (manifestDir: string | undefined, pnpmSettings: PnpmSettings, manifest?: ProjectManifest): OptionsFromRootManifest {
const settings: OptionsFromRootManifest = replaceEnvInSettings(pnpmSettings)
if (settings.overrides) {
assertValidOverrides(settings.overrides)
if (Object.keys(settings.overrides).length === 0) {
delete settings.overrides
} else if (manifest) {
settings.overrides = mapValues(createVersionReferencesReplacer(manifest), settings.overrides)
}
}
if (pnpmSettings.patchedDependencies) {
settings.patchedDependencies = { ...pnpmSettings.patchedDependencies }
for (const [dep, patchFile] of Object.entries(pnpmSettings.patchedDependencies)) {
if (manifestDir == null || path.isAbsolute(patchFile)) continue
settings.patchedDependencies[dep] = path.join(manifestDir, patchFile)
}
}
return settings
}
function assertValidOverrides (overrides: unknown): asserts overrides is Record<string, string> {
if (overrides == null || typeof overrides !== 'object' || Array.isArray(overrides)) {
throw new PnpmError('INVALID_OVERRIDES', `The overrides field should be an object, but got ${renderReceivedType(overrides)}`)
}
for (const [selector, spec] of Object.entries(overrides)) {
if (typeof spec !== 'string') {
throw new PnpmError('INVALID_OVERRIDES', `The value of overrides.${selector} should be a string, but got ${renderReceivedType(spec)}`)
}
}
}
function renderReceivedType (value: unknown): string {
if (value === null) return 'null'
if (Array.isArray(value)) return 'array'
return typeof value
}
function replaceEnvInSettings (settings: PnpmSettings): PnpmSettings {
const newSettings: PnpmSettings = {}
for (const [key, value] of Object.entries(settings)) {
const newKey = envReplace(key, process.env)
if (typeof value === 'string') {
// @ts-expect-error
newSettings[newKey as keyof PnpmSettings] = envReplace(value, process.env)
} else if (newKey === 'registries' || newKey === 'namedRegistries') {
// Registry URL maps in workspace yaml must support `${VAR}` substitution
// in their values so users can reuse the same env-var pattern they use
// in `.npmrc`. Only these keys are treated this way to avoid surprising
// behavior on unrelated object-valued settings.
newSettings[newKey as keyof PnpmSettings] = replaceEnvInStringValues(value) as never
} else {
newSettings[newKey as keyof PnpmSettings] = value
}
}
return newSettings
}
function replaceEnvInStringValues (value: unknown): unknown {
if (value == null || typeof value !== 'object' || Array.isArray(value)) return value
const out: Record<string, unknown> = {}
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
out[k] = typeof v === 'string' ? envReplace(v, process.env) : v
}
return out
}
function createVersionReferencesReplacer (manifest: ProjectManifest): (spec: string) => string {
const allDeps = {
...manifest.devDependencies,
...manifest.dependencies,
...manifest.optionalDependencies,
}
return replaceVersionReferences.bind(null, allDeps)
}
function replaceVersionReferences (dep: Record<string, string>, spec: string): string {
if (!(spec[0] === '$')) return spec
const dependencyName = spec.slice(1)
const newSpec = dep[dependencyName]
if (newSpec) return newSpec
throw new PnpmError(
'CANNOT_RESOLVE_OVERRIDE_VERSION',
`Cannot resolve version ${spec} in overrides. The direct dependencies don't have dependency "${dependencyName}".`
)
}