refactor(config): split Config interface into settings + runtime context (#11197)

* refactor(config): split Config interface into settings + runtime context

Create ConfigContext for runtime state (hooks, finders, workspace graph,
CLI metadata) and keep Config for user-facing settings only. Functions
use Pick<Config, ...> & Pick<ConfigContext, ...> to express which fields
they need from each interface.

getConfig() now returns { config, context, warnings }. The CLI wrapper
returns { config, context } and spreads both when calling command
handlers (to be refactored to separate params in follow-up PRs).

Closes #11195

* fix: address review feedback

- Initialize cliOptions on pnpmConfig so context.cliOptions is never undefined
- Move rootProjectManifestDir assignment before ignoreLocalSettings guard
- Add allProjectsGraph to INTERNAL_CONFIG_KEYS

* refactor: remove INTERNAL_CONFIG_KEYS from configToRecord

configToRecord now accepts Config and ConfigContext separately, so
context fields are never in scope. Only auth-related Config fields
(authConfig, authInfos, sslConfigs) need filtering.

* refactor: eliminate INTERNAL_CONFIG_KEYS from configToRecord

configToRecord now receives the clean Config object and explicitlySetKeys
separately (via opts.config and opts.context), so context fields are
never in scope. main.ts passes the original split objects alongside
the spread for command handlers that need them.

* fix: spelling

* fix: import sorting

* fix: --config.xxx nconf overrides conflicting with --config CLI flag

When `pnpm add` registers `config: Boolean`, nopt captures
--config.xxx=yyy as the --config flag value instead of treating it
as a nconf-style config override. Fix by extracting --config.xxx args
before nopt parsing and re-parsing them separately.

Also rename the split config/context properties on the command opts
object to _config/_context to avoid clashing with the --config CLI option.
This commit is contained in:
Zoltan Kochan
2026-04-04 23:44:25 +02:00
committed by GitHub
parent 96704a1c58
commit 3033bee430
66 changed files with 602 additions and 473 deletions

View File

@@ -1,7 +1,7 @@
import path from 'node:path'
import { DEFAULT_REGISTRIES, normalizeRegistries } from '@pnpm/config.normalize-registries'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import type { LogBase } from '@pnpm/logger'
import type { StoreController } from '@pnpm/store.controller-types'
import type { Registries } from '@pnpm/types'
@@ -55,7 +55,7 @@ export type StrictBuildOptions = {
} & Pick<Config, 'sslConfigs' | 'allowBuilds'>
export type BuildOptions = Partial<StrictBuildOptions> &
Pick<StrictBuildOptions, 'storeDir' | 'storeController'> & Pick<Config, 'rootProjectManifest' | 'rootProjectManifestDir'>
Pick<StrictBuildOptions, 'storeDir' | 'storeController'> & Pick<ConfigContext, 'rootProjectManifest' | 'rootProjectManifestDir'>
const defaults = async (opts: BuildOptions): Promise<StrictBuildOptions> => {
const packageManager = opts.packageManager ??

View File

@@ -4,7 +4,7 @@ import {
} from '@pnpm/building.after-install'
import { FILTERING, UNIVERSAL_OPTIONS } from '@pnpm/cli.common-cli-options-help'
import { docsUrl, readProjectManifestOnly } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import type { LogBase } from '@pnpm/logger'
import {
createStoreController,
@@ -75,23 +75,24 @@ For options that may be used with `-r`, see "pnpm help recursive"',
}
export type RebuildCommandOpts = Pick<Config,
| 'allProjects'
| 'dir'
| 'engineStrict'
| 'hooks'
| 'lockfileDir'
| 'nodeLinker'
| 'rawLocalConfig'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'registries'
| 'scriptShell'
| 'selectedProjectsGraph'
| 'sideEffectsCache'
| 'sideEffectsCacheReadonly'
| 'scriptsPrependNodePath'
| 'shellEmulator'
| 'workspaceDir'
> & Pick<ConfigContext,
| 'allProjects'
| 'hooks'
| 'rawLocalConfig'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'selectedProjectsGraph'
> &
CreateStoreControllerOptions &
{

View File

@@ -8,6 +8,7 @@ import {
} from '@pnpm/cli.utils'
import {
type Config,
type ConfigContext,
createProjectConfigRecord,
getWorkspaceConcurrency,
} from '@pnpm/config.reader'
@@ -19,18 +20,19 @@ import pLimit from 'p-limit'
type RecursiveRebuildOpts = CreateStoreControllerOptions & Pick<Config,
| 'hoistPattern'
| 'hooks'
| 'ignorePnpmfile'
| 'ignoreScripts'
| 'lockfileDir'
| 'lockfileOnly'
| 'nodeLinker'
| 'packageConfigs'
| 'rawLocalConfig'
| 'registries'
| 'sharedWorkspaceLockfile'
> & Pick<ConfigContext,
| 'hooks'
| 'rawLocalConfig'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'sharedWorkspaceLockfile'
> & {
pending?: boolean
} & Partial<Pick<Config, 'bail' | 'sort' | 'workspaceConcurrency'>>
@@ -40,7 +42,7 @@ export async function recursiveRebuild (
params: string[],
opts: RecursiveRebuildOpts & {
ignoredPackages?: Set<string>
} & Required<Pick<Config, 'selectedProjectsGraph' | 'workspaceDir'>>
} & Required<Pick<ConfigContext, 'selectedProjectsGraph'>> & Required<Pick<Config, 'workspaceDir'>>
): Promise<void> {
if (allProjects.length === 0) {
// It might make sense to throw an exception in this case

View File

@@ -1,5 +1,5 @@
import type { CommandHandlerMap } from '@pnpm/cli.command'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { writeSettings } from '@pnpm/config.writer'
import { parse } from '@pnpm/deps.path'
import { PnpmError } from '@pnpm/error'
@@ -14,7 +14,7 @@ import { renderHelp } from 'render-help'
import { rebuild, type RebuildCommandOpts } from '../build/index.js'
import { getAutomaticallyIgnoredBuilds } from './getAutomaticallyIgnoredBuilds.js'
export type ApproveBuildsCommandOpts = Pick<Config, 'modulesDir' | 'dir' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'allowBuilds' | 'enableGlobalVirtualStore'> & { all?: boolean, global?: boolean }
export type ApproveBuildsCommandOpts = Pick<Config, 'modulesDir' | 'dir' | 'allowBuilds' | 'enableGlobalVirtualStore'> & Pick<ConfigContext, 'rootProjectManifest' | 'rootProjectManifestDir'> & { all?: boolean, global?: boolean }
export const commandNames = ['approve-builds']

View File

@@ -41,11 +41,13 @@ async function getApproveBuildsConfig () {
dir: process.cwd(),
registry: `http://localhost:${REGISTRY_MOCK_PORT}`,
}
const { config, context } = await getConfig({
cliOptions,
packageManager: { name: 'pnpm', version: '' },
})
return {
...omit(['reporter'], (await getConfig({
cliOptions,
packageManager: { name: 'pnpm', version: '' },
})).config),
...omit(['reporter'], config),
...context,
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
pnpmfile: [], // this is only needed because the pnpmfile returned by getConfig is string | string[]

View File

@@ -7,7 +7,7 @@ import {
cacheView,
} from '@pnpm/cache.api'
import { docsUrl } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { ABBREVIATED_META_DIR, FULL_FILTERED_META_DIR } from '@pnpm/constants'
import { PnpmError } from '@pnpm/error'
import { getStorePath } from '@pnpm/store.path'
@@ -59,7 +59,7 @@ export function help (): string {
})
}
export type CacheCommandOptions = Pick<Config, 'cacheDir' | 'storeDir' | 'pnpmHomeDir' | 'cliOptions' | 'resolutionMode' | 'registrySupportsTimeField'>
export type CacheCommandOptions = Pick<Config, 'cacheDir' | 'storeDir' | 'pnpmHomeDir' | 'resolutionMode' | 'registrySupportsTimeField'> & Pick<ConfigContext, 'cliOptions'>
export async function handler (opts: CacheCommandOptions, params: string[]): Promise<string | undefined> {
const cacheType = (opts.resolutionMode === 'time-based' && !opts.registrySupportsTimeField)

View File

@@ -1,4 +1,4 @@
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import type * as logs from '@pnpm/core-loggers'
import type { LogLevel, StreamParser } from '@pnpm/logger'
import createDiffer from 'ansi-diff'
@@ -33,7 +33,7 @@ export function initDefaultReporter (
}
context: {
argv: string[]
config?: Config
config?: Config & ConfigContext
env?: NodeJS.ProcessEnv
process?: NodeJS.Process
}
@@ -111,7 +111,7 @@ export function toOutput$ (
}
context: {
argv: string[]
config?: Config
config?: Config & ConfigContext
env?: NodeJS.ProcessEnv
process?: NodeJS.Process
}

View File

@@ -1,4 +1,4 @@
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import type * as logs from '@pnpm/core-loggers'
import type { LogLevel } from '@pnpm/logger'
import type * as Rx from 'rxjs'
@@ -66,7 +66,7 @@ export function reporterForClient (
process: NodeJS.Process
isRecursive: boolean
logLevel?: LogLevel
pnpmConfig?: Config
pnpmConfig?: Config & ConfigContext
streamLifecycleOutput?: boolean
aggregateOutput?: boolean
throttleProgress?: number

View File

@@ -1,4 +1,4 @@
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import type { IgnoredScriptsLog } from '@pnpm/core-loggers'
import { lexCompare } from '@pnpm/util.lex-comparator'
import boxen from 'boxen'
@@ -10,7 +10,7 @@ export function reportIgnoredBuilds (
ignoredScripts: Rx.Observable<IgnoredScriptsLog>
},
opts: {
pnpmConfig?: Config
pnpmConfig?: Config & ConfigContext
// This is used by Bit CLI
approveBuildsInstructionText?: string
}

View File

@@ -2,7 +2,7 @@
import path from 'node:path'
import { toOutput$ } from '@pnpm/cli.default-reporter'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import {
deprecationLogger,
hookLogger,
@@ -43,7 +43,7 @@ test('prints summary (of current package only)', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: prefix } as Config,
config: { dir: prefix } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -235,7 +235,7 @@ test('prints summary without the filtered out entries', async () => {
argv: ['install'],
config: {
dir: prefix,
} as Config,
} as Config & ConfigContext,
},
streamParser: createStreamParser(),
filterPkgsDiff: (diff) => diff.name !== 'bar',
@@ -304,7 +304,7 @@ test('does not print deprecation message when log level is set to error', async
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: prefix } as Config,
config: { dir: prefix } as Config & ConfigContext,
},
reportingOptions: {
logLevel: 'error',
@@ -362,7 +362,7 @@ test('prints summary for global installation', async () => {
config: {
dir: prefix,
global: true,
} as Config,
} as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -419,7 +419,7 @@ test('prints added peer dependency', async () => {
argv: ['install'],
config: {
dir: prefix,
} as Config,
} as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -460,7 +460,7 @@ test('prints summary correctly when the same package is specified both in option
argv: ['install'],
config: {
dir: prefix,
} as Config,
} as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -522,7 +522,7 @@ test('in the installation summary report which dependency types are skipped', as
production: true,
dev: false,
optional: false,
} as Config,
} as Config & ConfigContext,
env: {
NODE_ENV: 'production',
},
@@ -583,7 +583,7 @@ ${h1('devDependencies:')} skipped
test('prints summary when some packages fail', async () => {
const output$ = toOutput$({
context: { argv: ['run'], config: { recursive: true } as Config },
context: { argv: ['run'], config: { recursive: true } as Config & ConfigContext },
streamParser: createStreamParser(),
})
@@ -822,7 +822,7 @@ test('prints added/removed stats and warnings during recursive installation', as
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: rootPrefix, recursive: true } as Config,
config: { dir: rootPrefix, recursive: true } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -881,7 +881,7 @@ test('recursive installation: prints only the added stats if nothing was removed
const output$ = toOutput$({
context: {
argv: ['recursive'],
config: { dir: '/home/jane/repo' } as Config,
config: { dir: '/home/jane/repo' } as Config & ConfigContext,
},
reportingOptions: { outputMaxWidth: 60 },
streamParser: createStreamParser(),
@@ -900,7 +900,7 @@ test('recursive installation: prints only the removed stats if nothing was added
const output$ = toOutput$({
context: {
argv: ['recursive'],
config: { dir: '/home/jane/repo' } as Config,
config: { dir: '/home/jane/repo' } as Config & ConfigContext,
},
reportingOptions: { outputMaxWidth: 60 },
streamParser: createStreamParser(),
@@ -919,7 +919,7 @@ test('recursive installation: prints at least one remove sign when removed !== 0
const output$ = toOutput$({
context: {
argv: ['recursive'],
config: { dir: '/home/jane/repo' } as Config,
config: { dir: '/home/jane/repo' } as Config & ConfigContext,
},
reportingOptions: { outputMaxWidth: 62 },
streamParser: createStreamParser(),
@@ -938,7 +938,7 @@ test('recursive installation: prints at least one add sign when added !== 0', as
const output$ = toOutput$({
context: {
argv: ['recursive'],
config: { dir: '/home/jane/repo' } as Config,
config: { dir: '/home/jane/repo' } as Config & ConfigContext,
},
reportingOptions: { outputMaxWidth: 62 },
streamParser: createStreamParser(),
@@ -957,7 +957,7 @@ test('recursive uninstall: prints removed packages number', async () => {
const output$ = toOutput$({
context: {
argv: ['remove'],
config: { dir: '/home/jane/repo', recursive: true } as Config,
config: { dir: '/home/jane/repo', recursive: true } as Config & ConfigContext,
},
reportingOptions: { outputMaxWidth: 62 },
streamParser: createStreamParser(),
@@ -975,7 +975,7 @@ test('install: print hook message', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: '/home/jane/repo' } as Config,
config: { dir: '/home/jane/repo' } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -997,7 +997,7 @@ test('recursive: print hook message', async () => {
const output$ = toOutput$({
context: {
argv: ['recursive'],
config: { dir: '/home/jane/repo' } as Config,
config: { dir: '/home/jane/repo' } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -1020,7 +1020,7 @@ test('prints skipped optional dependency info message', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: prefix } as Config,
config: { dir: prefix } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -1049,7 +1049,7 @@ test('logLevel=default', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: prefix } as Config,
config: { dir: prefix } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -1072,7 +1072,7 @@ test('logLevel=warn', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: prefix } as Config,
config: { dir: prefix } as Config & ConfigContext,
},
reportingOptions: {
logLevel: 'warn',
@@ -1097,7 +1097,7 @@ test('logLevel=error', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: prefix } as Config,
config: { dir: prefix } as Config & ConfigContext,
},
reportingOptions: {
logLevel: 'error',
@@ -1121,7 +1121,7 @@ test('warnings are collapsed', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: prefix } as Config,
config: { dir: prefix } as Config & ConfigContext,
},
reportingOptions: {
logLevel: 'warn',
@@ -1153,7 +1153,7 @@ test('warnings are not collapsed when append-only is true', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: prefix } as Config,
config: { dir: prefix } as Config & ConfigContext,
},
reportingOptions: {
appendOnly: true,

View File

@@ -1,5 +1,5 @@
import { toOutput$ } from '@pnpm/cli.default-reporter'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import {
deprecationLogger,
stageLogger,
@@ -17,7 +17,7 @@ test('prints summary of deprecated subdependencies', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: prefix } as Config,
config: { dir: prefix } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})

View File

@@ -1,5 +1,5 @@
import { toOutput$ } from '@pnpm/cli.default-reporter'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import {
fetchingProgressLogger,
progressLogger,
@@ -25,7 +25,7 @@ test('prints progress beginning', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: '/src/project' } as Config,
config: { dir: '/src/project' } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -50,7 +50,7 @@ test('prints progress without added packages stats', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: '/src/project' } as Config,
config: { dir: '/src/project' } as Config & ConfigContext,
},
reportingOptions: {
hideAddedPkgsProgress: true,
@@ -78,7 +78,7 @@ test('prints all progress stats', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: '/src/project' } as Config,
config: { dir: '/src/project' } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -119,7 +119,7 @@ test('prints progress beginning of node_modules from not cwd', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: '/src/projects' } as Config,
config: { dir: '/src/projects' } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -144,7 +144,7 @@ test('prints progress beginning of node_modules from not cwd, when progress pref
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: '/src/projects' } as Config,
config: { dir: '/src/projects' } as Config & ConfigContext,
},
streamParser: createStreamParser(),
reportingOptions: {
@@ -172,7 +172,7 @@ test('prints progress beginning when appendOnly is true', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: '/src/project' } as Config,
config: { dir: '/src/project' } as Config & ConfigContext,
},
reportingOptions: {
appendOnly: true,
@@ -203,7 +203,7 @@ test('prints progress beginning during recursive install', async () => {
config: {
dir: '/src/project',
recursive: true,
} as Config,
} as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -230,7 +230,7 @@ test('prints progress on first download', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: '/src/project' } as Config,
config: { dir: '/src/project' } as Config & ConfigContext,
},
reportingOptions: { throttleProgress: 0 },
streamParser: createStreamParser(),
@@ -264,7 +264,7 @@ test('moves fixed line to the end', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: prefix } as Config,
config: { dir: prefix } as Config & ConfigContext,
},
reportingOptions: { throttleProgress: 0 },
streamParser: createStreamParser(),
@@ -326,7 +326,7 @@ test('prints progress of big files download', async () => {
const output$ = toOutput$({
context: {
argv: ['install'],
config: { dir: '/src/project' } as Config,
config: { dir: '/src/project' } as Config & ConfigContext,
},
reportingOptions: { throttleProgress: 0 },
streamParser: createStreamParser(),

View File

@@ -1,7 +1,7 @@
import { setTimeout } from 'node:timers/promises'
import { toOutput$ } from '@pnpm/cli.default-reporter'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { scopeLogger } from '@pnpm/core-loggers'
import { createStreamParser } from '@pnpm/logger'
import { firstValueFrom } from 'rxjs'
@@ -33,7 +33,7 @@ test('prints scope of recursive install in a workspace when not all packages are
const output$ = toOutput$({
context: {
argv: ['install'],
config: { recursive: true } as Config,
config: { recursive: true } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -54,7 +54,7 @@ test('prints scope of recursive install in a workspace when all packages are sel
const output$ = toOutput$({
context: {
argv: ['install'],
config: { recursive: true } as Config,
config: { recursive: true } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -75,7 +75,7 @@ test('prints scope of recursive install not in a workspace when not all packages
const output$ = toOutput$({
context: {
argv: ['install'],
config: { recursive: true } as Config,
config: { recursive: true } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})
@@ -95,7 +95,7 @@ test('prints scope of recursive install not in a workspace when all packages are
const output$ = toOutput$({
context: {
argv: ['install'],
config: { recursive: true } as Config,
config: { recursive: true } as Config & ConfigContext,
},
streamParser: createStreamParser(),
})

View File

@@ -2,7 +2,7 @@ import { setTimeout } from 'node:timers/promises'
import { stripVTControlCharacters as stripAnsi } from 'node:util'
import { toOutput$ } from '@pnpm/cli.default-reporter'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { updateCheckLogger } from '@pnpm/core-loggers'
import { createStreamParser } from '@pnpm/logger'
import { firstValueFrom } from 'rxjs'
@@ -35,7 +35,7 @@ test('print update notification if the latest version is greater than the curren
const output$ = toOutput$({
context: {
argv: ['install'],
config: { recursive: true } as Config,
config: { recursive: true } as Config & ConfigContext,
env: {},
},
streamParser: createStreamParser(),
@@ -56,7 +56,7 @@ test('print update notification for Corepack if the latest version is greater th
const output$ = toOutput$({
context: {
argv: ['install'],
config: { recursive: true } as Config,
config: { recursive: true } as Config & ConfigContext,
env: {
COREPACK_ROOT: '/usr/bin/corepack',
},
@@ -79,7 +79,7 @@ test('print update notification that suggests to use the standalone scripts for
const output$ = toOutput$({
context: {
argv: ['install'],
config: { recursive: true } as Config,
config: { recursive: true } as Config & ConfigContext,
env: {
PNPM_HOME: '/home/user/.local/share/pnpm',
},

View File

@@ -129,6 +129,22 @@ export async function parseCliArgs (
return [noptExploratoryResults.argv.remain[indexOfRunScriptName]]
}
// When "config" is a registered CLI option (e.g. `pnpm add --config`),
// nopt captures --config.xxx=yyy as the "config" flag value instead of
// treating it as the nconf-style config override syntax. Work around this
// by rewriting --config.xxx=yyy to a placeholder before nopt, then restoring.
const hasConfigOption = 'config' in types
const configDotArgs: string[] = []
const filteredArgv = hasConfigOption
? inputArgv.map(arg => {
if (arg.startsWith('--config.')) {
configDotArgs.push(arg)
return undefined
}
return arg
}).filter((arg): arg is string => arg !== undefined)
: inputArgv
const { argv, ...options } = nopt(
{
recursive: Boolean,
@@ -138,10 +154,17 @@ export async function parseCliArgs (
...opts.universalShorthands,
...opts.shorthandsByCommandName[commandName],
},
inputArgv,
filteredArgv,
0,
{ escapeArgs: getEscapeArgsWithSpecialCases() }
)
// Re-parse extracted --config.xxx args through nopt so they get proper
// type coercion (e.g. "false" → false for Boolean settings).
if (configDotArgs.length > 0) {
const { argv: _, ...configOptions } = nopt({}, {}, configDotArgs, 0)
Object.assign(options, configOptions)
}
const workspaceDir = await getWorkspaceDir(options)
// For the run command, it's not clear whether --help should be passed to the

View File

@@ -1,16 +1,16 @@
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
export type ConfigCommandOptions = Pick<Config,
| 'configDir'
| 'cliOptions'
| 'dir'
| 'global'
| 'authConfig'
| 'workspaceDir'
> & Pick<ConfigContext,
| 'cliOptions'
> & {
_config: Config
_context: ConfigContext
json?: boolean
location?: 'global' | 'project'
// The config commands receive the full Config object at runtime
// and read arbitrary typed properties for display.
[key: string]: unknown
}

View File

@@ -1,4 +1,4 @@
import { type Config, isIniConfigKey, types } from '@pnpm/config.reader'
import { isIniConfigKey, types } from '@pnpm/config.reader'
import { getObjectValueByPropertyPath } from '@pnpm/object.property-path'
import { isCamelCase } from '@pnpm/text.naming-cases'
import camelcase from 'camelcase'
@@ -28,9 +28,9 @@ function lookupConfig (opts: ConfigCommandOptions, key: string, isScopedKey: boo
// then fall back to authConfig (for keys like registry set in .npmrc)
if (Object.hasOwn(types, kebabKey)) {
const camelKey = camelcase(kebabKey, { locale: 'en-US' })
const explicit = (opts as unknown as Config).explicitlySetKeys
const explicit = opts._context.explicitlySetKeys
if (!explicit || explicit.has(camelKey)) {
return { value: (opts as unknown as Record<string, unknown>)[camelKey] }
return { value: (opts._config as unknown as Record<string, unknown>)[camelKey] }
}
// Fall back to authConfig for INI keys (registry, ca, etc.)
if (kebabKey in opts.authConfig) {
@@ -45,7 +45,7 @@ function lookupConfig (opts: ConfigCommandOptions, key: string, isScopedKey: boo
// For keys not in types (e.g., package-extensions), look up via configToRecord
// which excludes internal/sensitive fields.
const camelKey = camelcase(key, { locale: 'en-US' })
const record = configToRecord(opts as unknown as Config)
const record = configToRecord(opts._config, opts._context.explicitlySetKeys)
if (Object.hasOwn(record, camelKey)) {
return { value: record[camelKey] }
}
@@ -55,9 +55,9 @@ function lookupConfig (opts: ConfigCommandOptions, key: string, isScopedKey: boo
function lookupByPropertyPath (opts: ConfigCommandOptions, propertyPath: string): Found<unknown> {
const parsedPropertyPath = Array.from(parseConfigPropertyPath(propertyPath))
if (parsedPropertyPath.length === 0) {
return { value: configToRecord(opts as unknown as Config) }
return { value: configToRecord(opts._config, opts._context.explicitlySetKeys) }
}
const record = configToRecord(opts as unknown as Config)
const record = configToRecord(opts._config, opts._context.explicitlySetKeys)
return {
value: getObjectValueByPropertyPath(record, parsedPropertyPath),
}

View File

@@ -1,8 +1,6 @@
import type { Config } from '@pnpm/config.reader'
import type { ConfigCommandOptions } from './ConfigCommandOptions.js'
import { configToRecord } from './configToRecord.js'
export async function configList (opts: ConfigCommandOptions): Promise<string> {
return JSON.stringify(configToRecord(opts as unknown as Config), undefined, 2)
return JSON.stringify(configToRecord(opts._config, opts._context.explicitlySetKeys), undefined, 2)
}

View File

@@ -4,26 +4,25 @@ import camelcase from 'camelcase'
import { censorProtectedSettings } from './protectedSettings.js'
const INTERNAL_CONFIG_KEYS = new Set([
'authConfig', 'authInfos', 'rawLocalConfig', 'cliOptions',
'explicitlySetKeys',
'hooks', 'finders', 'allProjects', 'selectedProjectsGraph',
'packageManager', 'wantedPackageManager', 'rootProjectManifest',
'storeController', 'rootProjectManifestDir', 'sslConfigs',
// Auth-related Config fields that are internal objects, not user settings.
const NON_SETTING_CONFIG_KEYS = new Set([
'authConfig', 'authInfos', 'sslConfigs',
])
/**
* Convert a Config object to a camelCase record for display.
* Only includes explicitly set values (from CLI, env vars, or workspace yaml),
* not default values. Auth/registry keys from authConfig are always included.
*
* Accepts a clean Config object (without ConfigContext fields mixed in),
* so no INTERNAL_CONFIG_KEYS exclusion list is needed.
*/
export function configToRecord (config: Config): Record<string, unknown> {
export function configToRecord (config: Config, explicitlySetKeys: Set<string>): Record<string, unknown> {
const result: Record<string, unknown> = {}
const explicit = config.explicitlySetKeys
// Add typed settings (only explicitly set ones if tracking is available)
for (const kebabKey of Object.keys(types)) {
const camelKey = camelcase(kebabKey, { locale: 'en-US' })
if (explicit && !explicit.has(camelKey)) continue
if (!explicitlySetKeys.has(camelKey)) continue
const value = (config as unknown as Record<string, unknown>)[camelKey]
if (value !== undefined) {
result[camelKey] = value
@@ -31,8 +30,8 @@ export function configToRecord (config: Config): Record<string, unknown> {
}
// Add non-types config properties (e.g., packageExtensions, overrides)
for (const [key, value] of Object.entries(config)) {
if (value === undefined || INTERNAL_CONFIG_KEYS.has(key)) continue
if (!(key in result) && (!explicit || explicit.has(key))) {
if (value === undefined || NON_SETTING_CONFIG_KEYS.has(key)) continue
if (!(key in result) && explicitlySetKeys.has(key)) {
result[key] = value
}
}

View File

@@ -7,19 +7,21 @@ import { readIniFileSync } from 'read-ini-file'
import { readYamlFileSync } from 'read-yaml-file'
import { writeYamlFileSync } from 'write-yaml-file'
import { createConfigCommandOpts } from './utils/index.js'
test('config delete on registry key not set', async () => {
const tmp = tempDir()
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(configDir, 'auth.ini'), '@my-company:registry=https://registry.my-company.example.com/')
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['delete', 'registry'])
}), ['delete', 'registry'])
expect(readIniFileSync(path.join(configDir, 'auth.ini'))).toEqual({
'@my-company:registry': 'https://registry.my-company.example.com/',
@@ -32,13 +34,13 @@ test('config delete on registry key set', async () => {
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(configDir, 'auth.ini'), 'registry=https://registry.my-company.example.com/')
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['delete', 'registry'])
}), ['delete', 'registry'])
expect(readIniFileSync(path.join(configDir, 'auth.ini'))).toEqual({})
})
@@ -49,13 +51,13 @@ test('config delete on npm-compatible key not set', async () => {
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(configDir, 'auth.ini'), '@my-company:registry=https://registry.my-company.example.com/')
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['delete', 'cafile'])
}), ['delete', 'cafile'])
expect(readIniFileSync(path.join(configDir, 'auth.ini'))).toEqual({
'@my-company:registry': 'https://registry.my-company.example.com/',
@@ -68,13 +70,13 @@ test('config delete on npm-compatible key set', async () => {
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(configDir, 'auth.ini'), 'cafile=some-cafile')
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['delete', 'cafile'])
}), ['delete', 'cafile'])
// NOTE: pnpm currently does not delete empty rc files.
// TODO: maybe we should?
@@ -89,13 +91,13 @@ test('config delete on pnpm-specific key not set', async () => {
cacheDir: '~/cache',
})
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['delete', 'store-dir'])
}), ['delete', 'store-dir'])
expect(readYamlFileSync(path.join(configDir, 'config.yaml'))).toStrictEqual({
cacheDir: '~/cache',
@@ -110,13 +112,13 @@ test('config delete on pnpm-specific key set', async () => {
cacheDir: '~/cache',
})
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['delete', 'cache-dir'])
}), ['delete', 'cache-dir'])
expect(fs.readdirSync(configDir)).not.toContain('config.yaml')
})

View File

@@ -1,48 +1,48 @@
import { config } from '@pnpm/config.commands'
import { getOutputString } from './utils/index.js'
import { createConfigCommandOpts, getOutputString } from './utils/index.js'
test('config get', async () => {
const getResult = await config.handler({
const getResult = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
global: true,
authConfig: {},
storeDir: '~/store',
}, ['get', 'store-dir'])
}), ['get', 'store-dir'])
expect(getOutputString(getResult)).toBe('~/store')
})
test('config get works with camelCase', async () => {
const getResult = await config.handler({
const getResult = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
global: true,
authConfig: {},
storeDir: '~/store',
}, ['get', 'storeDir'])
}), ['get', 'storeDir'])
expect(getOutputString(getResult)).toBe('~/store')
})
test('config get a boolean should return string format', async () => {
const getResult = await config.handler({
const getResult = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
global: true,
authConfig: {},
updateNotifier: true,
}, ['get', 'update-notifier'])
}), ['get', 'update-notifier'])
expect(getOutputString(getResult)).toBe('true')
})
test('config get on array should return a comma-separated list', async () => {
const getResult = await config.handler({
const getResult = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
@@ -52,7 +52,7 @@ test('config get on array should return a comma-separated list', async () => {
'*eslint*',
'*prettier*',
],
}, ['get', 'public-hoist-pattern'])
}), ['get', 'public-hoist-pattern'])
expect(JSON.parse(getOutputString(getResult))).toStrictEqual([
'*eslint*',
@@ -61,7 +61,7 @@ test('config get on array should return a comma-separated list', async () => {
})
test('config get on object should return a JSON string', async () => {
const getResult = await config.handler({
const getResult = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
@@ -70,7 +70,7 @@ test('config get on object should return a JSON string', async () => {
catalog: {
react: '^19.0.0',
},
}, ['get', 'catalog'])
}), ['get', 'catalog'])
expect(JSON.parse(getOutputString(getResult))).toStrictEqual({ react: '^19.0.0' })
})
@@ -80,20 +80,15 @@ test('config get without key show list all settings', async () => {
'store-dir': '~/store',
'fetch-retries': '2',
}
const getOutput = await config.handler({
const baseOpts = {
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
global: true,
authConfig,
}, ['get'])
}
const getOutput = await config.handler(createConfigCommandOpts(baseOpts), ['get'])
const listOutput = await config.handler({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
authConfig,
}, ['list'])
const listOutput = await config.handler(createConfigCommandOpts(baseOpts), ['list'])
expect(getOutput).toStrictEqual(listOutput)
})
@@ -116,14 +111,14 @@ describe('config get with a property path', () => {
trustPolicyExclude: ['foo', 'bar'],
packageExtensions,
}
const baseOpts = {
const baseOpts = createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
global: true,
authConfig: {},
...configData,
}
})
describe('anything with --json', () => {
test('«»', async () => {
@@ -211,7 +206,7 @@ describe('config get with a property path', () => {
})
test('config get with scoped registry key (global: false)', async () => {
const getResult = await config.handler({
const getResult = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
@@ -219,13 +214,13 @@ test('config get with scoped registry key (global: false)', async () => {
authConfig: {
'@scope:registry': 'https://custom-registry.example.com/',
},
}, ['get', '@scope:registry'])
}), ['get', '@scope:registry'])
expect(getOutputString(getResult)).toBe('https://custom-registry.example.com/')
})
test('config get with scoped registry key (global: true)', async () => {
const getResult = await config.handler({
const getResult = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
@@ -233,19 +228,19 @@ test('config get with scoped registry key (global: true)', async () => {
authConfig: {
'@scope:registry': 'https://custom-registry.example.com/',
},
}, ['get', '@scope:registry'])
}), ['get', '@scope:registry'])
expect(getOutputString(getResult)).toBe('https://custom-registry.example.com/')
})
test('config get with scoped registry key that does not exist', async () => {
const getResult = await config.handler({
const getResult = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
global: false,
authConfig: {},
}, ['get', '@scope:registry'])
}), ['get', '@scope:registry'])
expect(getOutputString(getResult)).toBe('undefined')
})
@@ -261,13 +256,13 @@ describe('does not traverse the prototype chain (#10296)', () => {
'valueOf',
'__proto__',
])('%s', async key => {
const getResult = await config.handler({
const getResult = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
global: true,
authConfig: {},
}, ['get', key])
}), ['get', key])
expect(getOutputString(getResult)).toBe('undefined')
})

View File

@@ -1,16 +1,16 @@
import { config } from '@pnpm/config.commands'
import { getOutputString } from './utils/index.js'
import { createConfigCommandOpts, getOutputString } from './utils/index.js'
test('config list', async () => {
const output = await config.handler({
const output = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
authConfig: {},
storeDir: '~/store',
fetchRetries: '2',
}, ['list'])
}), ['list'])
expect(JSON.parse(getOutputString(output))).toMatchObject({
fetchRetries: '2',
@@ -19,7 +19,7 @@ test('config list', async () => {
})
test('config list --json', async () => {
const output = await config.handler({
const output = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
@@ -27,7 +27,7 @@ test('config list --json', async () => {
authConfig: {},
storeDir: '~/store',
fetchRetries: '2',
}, ['list'])
}), ['list'])
const parsed = JSON.parse(output as string)
expect(parsed).toMatchObject({
@@ -43,14 +43,14 @@ test('config list censors protected settings', async () => {
'//my-org.example.com:username': 'my-username-in-my-org',
}
const output = await config.handler({
const output = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir: process.cwd(),
storeDir: '~/store',
fetchRetries: '2',
authConfig,
}, ['list'])
}), ['list'])
expect(JSON.parse(getOutputString(output))).toMatchObject({
storeDir: '~/store',
@@ -68,7 +68,7 @@ test('config list --json censors protected settings', async () => {
'//my-org.example.com:username': 'my-username-in-my-org',
}
const output = await config.handler({
const output = await config.handler(createConfigCommandOpts({
dir: process.cwd(),
json: true,
cliOptions: {},
@@ -76,7 +76,7 @@ test('config list --json censors protected settings', async () => {
storeDir: '~/store',
fetchRetries: '2',
authConfig,
}, ['list'])
}), ['list'])
expect(JSON.parse(getOutputString(output))).toMatchObject({
storeDir: '~/store',

View File

@@ -7,7 +7,7 @@ import { tempDir } from '@pnpm/prepare'
import { readIniFileSync } from 'read-ini-file'
import { readYamlFileSync } from 'read-yaml-file'
import { type ConfigFilesData, readConfigFiles, writeConfigFiles } from './utils/index.js'
import { type ConfigFilesData, createConfigCommandOpts, readConfigFiles, writeConfigFiles } from './utils/index.js'
test('config set registry setting using the global option', async () => {
const tmp = tempDir()
@@ -24,13 +24,13 @@ test('config set registry setting using the global option', async () => {
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', 'registry', 'https://npm-registry.example.com/'])
}), ['set', 'registry', 'https://npm-registry.example.com/'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -56,13 +56,13 @@ test('config set npm-compatible setting using the global option', async () => {
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', 'cafile', 'some-cafile'])
}), ['set', 'cafile', 'some-cafile'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -88,13 +88,13 @@ test('config set pnpm-specific key using the global option', async () => {
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', 'fetch-retries', '1'])
}), ['set', 'fetch-retries', '1'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -120,13 +120,13 @@ test('config set using the location=global option', async () => {
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'global',
authConfig: {},
}, ['set', 'fetchRetries', '1'])
}), ['set', 'fetchRetries', '1'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -152,13 +152,13 @@ test('config set pnpm-specific setting using the location=project option', async
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', 'virtual-store-dir', '.pnpm'])
}), ['set', 'virtual-store-dir', '.pnpm'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -174,25 +174,25 @@ test('config delete with location=project, when delete the last setting from pnp
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', 'virtual-store-dir', '.pnpm'])
}), ['set', 'virtual-store-dir', '.pnpm'])
expect(readYamlFileSync(path.join(tmp, 'pnpm-workspace.yaml'))).toEqual({
virtualStoreDir: '.pnpm',
})
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['delete', 'virtual-store-dir'])
}), ['delete', 'virtual-store-dir'])
expect(fs.existsSync(path.join(tmp, 'pnpm-workspace.yaml'))).toBeFalsy()
})
@@ -213,13 +213,13 @@ test('config set registry setting using the location=project option', async () =
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', 'registry', 'https://npm-registry.example.com/'])
}), ['set', 'registry', 'https://npm-registry.example.com/'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -246,13 +246,13 @@ test('config set npm-compatible setting using the location=project option', asyn
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', 'cafile', 'some-cafile'])
}), ['set', 'cafile', 'some-cafile'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -268,13 +268,13 @@ test('config set saves the setting in the right format to pnpm-workspace.yaml',
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', 'fetch-timeout', '1000'])
}), ['set', 'fetch-timeout', '1000'])
expect(readYamlFileSync(path.join(tmp, 'pnpm-workspace.yaml'))).toEqual({
fetchTimeout: 1000,
@@ -300,14 +300,14 @@ test('config set registry setting in project .npmrc file', async () => {
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: false,
location: 'project',
authConfig: {},
}, ['set', 'registry', 'https://npm-registry.example.com/'])
}), ['set', 'registry', 'https://npm-registry.example.com/'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -337,14 +337,14 @@ test('config set npm-compatible setting in project .npmrc file', async () => {
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: false,
location: 'project',
authConfig: {},
}, ['set', 'cafile', 'some-cafile'])
}), ['set', 'cafile', 'some-cafile'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -374,14 +374,14 @@ test('config set pnpm-specific setting in project pnpm-workspace.yaml file', asy
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: false,
location: 'project',
authConfig: {},
}, ['set', 'fetch-retries', '1'])
}), ['set', 'fetch-retries', '1'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -411,13 +411,13 @@ test('config set key=value', async () => {
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', 'fetch-retries=1'])
}), ['set', 'fetch-retries=1'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -447,13 +447,13 @@ test('config set key=value, when value contains a "="', async () => {
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', 'lockfile-dir=foo=bar'])
}), ['set', 'lockfile-dir=foo=bar'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -470,21 +470,21 @@ test('config set or delete throws missing params error', async () => {
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(tmp, '.npmrc'), 'store-dir=~/store')
await expect(config.handler({
await expect(config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set'])).rejects.toThrow(new PnpmError('CONFIG_NO_PARAMS', '`pnpm config set` requires the config key'))
}), ['set'])).rejects.toThrow(new PnpmError('CONFIG_NO_PARAMS', '`pnpm config set` requires the config key'))
await expect(config.handler({
await expect(config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['delete'])).rejects.toThrow(new PnpmError('CONFIG_NO_PARAMS', '`pnpm config delete` requires the config key'))
}), ['delete'])).rejects.toThrow(new PnpmError('CONFIG_NO_PARAMS', '`pnpm config delete` requires the config key'))
})
test('config set with dot leading key', async () => {
@@ -500,13 +500,13 @@ test('config set with dot leading key', async () => {
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', '.fetchRetries', '1'])
}), ['set', '.fetchRetries', '1'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -530,13 +530,13 @@ test('config set with subscripted key', async () => {
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', '["fetch-retries"]', '1'])
}), ['set', '["fetch-retries"]', '1'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -553,13 +553,13 @@ test('config set rejects complex property path', async () => {
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(configDir, 'auth.ini'), 'store-dir=~/store')
await expect(config.handler({
await expect(config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', '.catalog.react', '19'])).rejects.toMatchObject({
}), ['set', '.catalog.react', '19'])).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_SET_DEEP_KEY',
})
})
@@ -569,14 +569,14 @@ test('config set with location=project and json=true', async () => {
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
json: true,
authConfig: {},
}, ['set', 'catalog', '{ "react": "19" }'])
}), ['set', 'catalog', '{ "react": "19" }'])
expect(readYamlFileSync(path.join(tmp, 'pnpm-workspace.yaml'))).toStrictEqual({
catalog: {
@@ -584,14 +584,14 @@ test('config set with location=project and json=true', async () => {
},
})
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
json: true,
authConfig: {},
}, ['set', 'packageExtensions', JSON.stringify({
}), ['set', 'packageExtensions', JSON.stringify({
'@babel/parser': {
peerDependencies: {
'@babel/types': '*',
@@ -636,26 +636,26 @@ test('config set refuses writing workspace-specific settings to the global confi
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await expect(config.handler({
await expect(config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'global',
json: true,
authConfig: {},
}, ['set', 'catalog', '{ "react": "19" }'])).rejects.toMatchObject({
}), ['set', 'catalog', '{ "react": "19" }'])).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_SET_UNSUPPORTED_YAML_CONFIG_KEY',
key: 'catalog',
})
await expect(config.handler({
await expect(config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'global',
json: true,
authConfig: {},
}, ['set', 'packageExtensions', JSON.stringify({
}), ['set', 'packageExtensions', JSON.stringify({
'@babel/parser': {
peerDependencies: {
'@babel/types': '*',
@@ -671,14 +671,14 @@ test('config set refuses writing workspace-specific settings to the global confi
key: 'packageExtensions',
})
await expect(config.handler({
await expect(config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'global',
json: true,
authConfig: {},
}, ['set', 'package-extensions', JSON.stringify({
}), ['set', 'package-extensions', JSON.stringify({
'@babel/parser': {
peerDependencies: {
'@babel/types': '*',
@@ -709,14 +709,14 @@ test('config set writes workspace-specific settings to pnpm-workspace.yaml', asy
writeConfigFiles(configDir, tmp, initConfig)
const catalog = { react: '19' }
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
json: true,
authConfig: {},
}, ['set', 'catalog', JSON.stringify(catalog)])
}), ['set', 'catalog', JSON.stringify(catalog)])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
localYaml: {
@@ -737,14 +737,14 @@ test('config set writes workspace-specific settings to pnpm-workspace.yaml', asy
},
},
}
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
json: true,
authConfig: {},
}, ['set', 'packageExtensions', JSON.stringify(packageExtensions)])
}), ['set', 'packageExtensions', JSON.stringify(packageExtensions)])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
localYaml: {
@@ -760,14 +760,14 @@ test('config set refuses kebab-case workspace-specific settings', async () => {
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
await expect(config.handler({
await expect(config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
json: true,
authConfig: {},
}, ['set', 'package-extensions', JSON.stringify({
}), ['set', 'package-extensions', JSON.stringify({
'@babel/parser': {
peerDependencies: {
'@babel/types': '*',
@@ -789,13 +789,13 @@ test('config set registry-specific setting with --location=project should create
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', '//registry.example.com/:_auth', 'test-auth-value'])
}), ['set', '//registry.example.com/:_auth', 'test-auth-value'])
expect(readIniFileSync(path.join(tmp, '.npmrc'))).toEqual({
'//registry.example.com/:_auth': 'test-auth-value',
@@ -808,13 +808,13 @@ test('config set scoped registry with --location=project should create .npmrc',
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', '@myorg:registry', 'https://test-registry.example.com/'])
}), ['set', '@myorg:registry', 'https://test-registry.example.com/'])
expect(readIniFileSync(path.join(tmp, '.npmrc'))).toEqual({
'@myorg:registry': 'https://test-registry.example.com/',
@@ -831,13 +831,13 @@ test('config set when both pnpm-workspace.yaml and .npmrc exist, pnpm-workspace.
fs.writeFileSync(path.join(tmp, '.npmrc'), 'store-dir=~/store')
fs.writeFileSync(path.join(tmp, 'pnpm-workspace.yaml'), 'fetchRetries: 5')
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', 'fetch-timeout', '2000'])
}), ['set', 'fetch-timeout', '2000'])
expect(readYamlFileSync(path.join(tmp, 'pnpm-workspace.yaml'))).toEqual({
fetchRetries: 5,
@@ -856,13 +856,13 @@ test('config set when only pnpm-workspace.yaml exists, writes to it', async () =
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(tmp, 'pnpm-workspace.yaml'), 'fetchRetries: 5')
await config.handler({
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
authConfig: {},
}, ['set', 'fetch-timeout', '3000'])
}), ['set', 'fetch-timeout', '3000'])
expect(readYamlFileSync(path.join(tmp, 'pnpm-workspace.yaml'))).toEqual({
fetchRetries: 5,

View File

@@ -3,6 +3,7 @@ import path from 'node:path'
import { config } from '@pnpm/config.commands'
import { tempDir } from '@pnpm/prepare'
import { createConfigCommandOpts } from './utils/index.js'
import { type ConfigFilesData, readConfigFiles, writeConfigFiles } from './utils/index.js'
describe.each(
@@ -27,13 +28,13 @@ describe.each(
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: tmp,
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', `${key}=123`])
}), ['set', `${key}=123`])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -51,13 +52,13 @@ describe.each(
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: tmp,
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['delete', key])
}), ['delete', key])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -78,14 +79,14 @@ describe.each(
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
json: true,
dir: tmp,
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', key, '"123"'])
}), ['set', key, '"123"'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -103,14 +104,14 @@ describe.each(
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
json: true,
dir: tmp,
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['delete', key])
}), ['delete', key])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -136,13 +137,13 @@ describe.each(
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: tmp,
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', `${key}=https://registry.example.com/`])
}), ['set', `${key}=https://registry.example.com/`])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -160,13 +161,13 @@ describe.each(
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: tmp,
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['delete', key])
}), ['delete', key])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -189,14 +190,14 @@ describe.each(
const tmp = tempDir()
const configDir = path.join(tmp, 'global-config')
it(`${key} should reject a non-string value`, async () => {
await expect(config.handler({
await expect(config.handler(createConfigCommandOpts({
json: true,
dir: tmp,
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', key, '{}'])).rejects.toMatchObject({
}), ['set', key, '{}'])).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_SET_AUTH_NON_STRING',
})
})
@@ -219,13 +220,13 @@ describe.each(
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: tmp,
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['set', propertyPath, '123'])
}), ['set', propertyPath, '123'])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,
@@ -243,13 +244,13 @@ describe.each(
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler({
await config.handler(createConfigCommandOpts({
dir: tmp,
cliOptions: {},
configDir,
global: true,
authConfig: {},
}, ['delete', propertyPath])
}), ['delete', propertyPath])
expect(readConfigFiles(configDir, tmp)).toEqual({
...initConfig,

View File

@@ -1,13 +1,46 @@
import fs from 'node:fs'
import path from 'node:path'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { readIniFileSync } from 'read-ini-file'
import { readYamlFileSync } from 'read-yaml-file'
import { writeIniFileSync } from 'write-ini-file'
import { writeYamlFileSync } from 'write-yaml-file'
import type { ConfigCommandOptions } from '../../src/ConfigCommandOptions.js'
import type { config } from '../../src/index.js'
/**
* Build a {@link ConfigCommandOptions} object for tests.
*
* Accepts the flat shape that tests already use (settings like `storeDir`,
* `authConfig`, etc. mixed into a single object) and builds the `config`
* and `context` properties that the refactored config commands now expect.
*/
export function createConfigCommandOpts (
opts: Record<string, unknown> & {
dir: string
configDir: string
cliOptions: Record<string, unknown>
authConfig: Record<string, unknown>
global?: boolean
json?: boolean
location?: 'global' | 'project'
}
): ConfigCommandOptions {
return {
...opts,
_config: opts as unknown as Config,
_context: {
cliOptions: opts.cliOptions ?? {},
explicitlySetKeys: new Set(Object.keys(opts)),
rawLocalConfig: {},
rootProjectManifestDir: opts.dir,
packageManager: { name: 'pnpm', version: '0.0.0' },
} as ConfigContext,
} as ConfigCommandOptions
}
export function getOutputString (result: config.ConfigHandlerResult): string {
if (result == null) throw new Error('output is null or undefined')
if (typeof result === 'string') return result

View File

@@ -14,22 +14,50 @@ import type {
import type { OptionsFromRootManifest } from './getOptionsFromRootManifest.js'
import type { AuthInfo } from './parseAuthInfo.js'
export type UniversalOptions = Pick<Config, 'color' | 'dir' | 'authConfig' | 'rawLocalConfig'>
export type UniversalOptions = Pick<Config, 'color' | 'dir' | 'authConfig'> & Pick<ConfigContext, 'rawLocalConfig'>
export type VerifyDepsBeforeRun = 'install' | 'warn' | 'error' | 'prompt' | false
export interface Config extends AuthInfo, OptionsFromRootManifest {
/**
* Runtime state, workspace context, and CLI metadata.
* These fields are NOT user-facing settings — they are computed at startup
* or populated later by the CLI harness (e.g. workspace filtering, hook loading).
*/
export interface ConfigContext {
// -- Runtime state --
hooks?: Hooks
finders?: Record<string, Finder>
// -- Workspace context --
allProjects?: Project[]
selectedProjectsGraph?: ProjectsGraph
allProjectsGraph?: ProjectsGraph
rootProjectManifest?: ProjectManifest
rootProjectManifestDir: string
// -- CLI metadata --
cliOptions: Record<string, any> // eslint-disable-line
rawLocalConfig: Record<string, any> // eslint-disable-line
/** Keys explicitly set from workspace yaml, CLI, or env vars (not defaults). */
explicitlySetKeys: Set<string>
packageManager: {
name: string
version: string
}
wantedPackageManager?: EngineDependency
}
/**
* User-facing settings + auth/network config.
* Does NOT include runtime state — see {@link ConfigContext} for that.
*/
export interface Config extends AuthInfo, OptionsFromRootManifest {
allowNew: boolean
autoConfirmAllPrompts?: boolean
autoInstallPeers?: boolean
bail: boolean
color: 'always' | 'auto' | 'never'
cliOptions: Record<string, any>, // eslint-disable-line
useBetaCli: boolean
excludeLinksFromLockfile: boolean
extraBinPaths: string[]
@@ -37,10 +65,7 @@ export interface Config extends AuthInfo, OptionsFromRootManifest {
failIfNoMatch: boolean
filter: string[]
filterProd: string[]
rawLocalConfig: Record<string, any>, // eslint-disable-line
authConfig: Record<string, any>, // eslint-disable-line
/** Keys explicitly set from workspace yaml, CLI, or env vars (not defaults). */
explicitlySetKeys: Set<string>
dryRun?: boolean // This option might be not supported ever
global?: boolean
dir: string
@@ -86,11 +111,6 @@ export interface Config extends AuthInfo, OptionsFromRootManifest {
frozenLockfile?: boolean
preferFrozenLockfile?: boolean
only?: 'prod' | 'production' | 'dev' | 'development'
packageManager: {
name: string
version: string
}
wantedPackageManager?: EngineDependency
preferOffline?: boolean
sideEffectsCache?: boolean // for backward compatibility
sideEffectsCacheReadonly?: boolean // for backward compatibility
@@ -144,8 +164,6 @@ export interface Config extends AuthInfo, OptionsFromRootManifest {
ignorePnpmfile?: boolean
pnpmfile: string[] | string
tryLoadDefaultPnpmfile?: boolean
hooks?: Hooks
finders?: Record<string, Finder>
packageImportMethod?: 'auto' | 'hardlink' | 'copy' | 'clone' | 'clone-or-copy'
hoistPattern?: string[]
publicHoistPattern?: string[] | string
@@ -203,8 +221,6 @@ export interface Config extends AuthInfo, OptionsFromRootManifest {
testPattern?: string[]
changedFilesIgnorePattern?: string[]
rootProjectManifestDir: string
rootProjectManifest?: ProjectManifest
userConfig: Record<string, string>
hoist: boolean

View File

@@ -1,40 +1,48 @@
import { inheritAuthConfig } from './auth.js'
import type { InheritableConfig } from './inheritPickedConfig.js'
import type { InheritableConfigPair } from './inheritPickedConfig.js'
test('inheritAuthConfig copies only auth keys from source to target', () => {
const target: InheritableConfig = {
bin: 'foo',
cacheDir: '/path/to/cache/dir',
registry: 'https://npmjs.com/registry/',
authConfig: {
'cache-dir': '/path/to/cache/dir',
registry: 'https://npmjs.com/registry/',
},
rawLocalConfig: {
const target: InheritableConfigPair = {
config: {
bin: 'foo',
cacheDir: '/path/to/cache/dir',
registry: 'https://npmjs.com/registry/',
authConfig: {
'cache-dir': '/path/to/cache/dir',
registry: 'https://npmjs.com/registry/',
},
} as any, // eslint-disable-line
context: {
rawLocalConfig: {
bin: 'foo',
registry: 'https://npmjs.com/registry/',
},
},
}
inheritAuthConfig(target, {
bin: 'bar',
cacheDir: '/path/to/another/cache/dir',
storeDir: '/path/to/custom/store/dir',
registry: 'https://example.com/local-registry/',
authConfig: {
registry: 'https://example.com/global-registry/',
'//example.com/global-registry/:_auth': 'MY_SECRET_GLOBAL_AUTH',
},
rawLocalConfig: {
config: {
bin: 'bar',
'cache-dir': '/path/to/another/cache/dir',
'store-dir': '/path/to/custom/store/dir',
cacheDir: '/path/to/another/cache/dir',
storeDir: '/path/to/custom/store/dir',
registry: 'https://example.com/local-registry/',
'//example.com/local-registry/:_authToken': 'MY_SECRET_LOCAL_AUTH',
authConfig: {
registry: 'https://example.com/global-registry/',
'//example.com/global-registry/:_auth': 'MY_SECRET_GLOBAL_AUTH',
},
} as any, // eslint-disable-line
context: {
rawLocalConfig: {
bin: 'bar',
'cache-dir': '/path/to/another/cache/dir',
'store-dir': '/path/to/custom/store/dir',
registry: 'https://example.com/local-registry/',
'//example.com/local-registry/:_authToken': 'MY_SECRET_LOCAL_AUTH',
},
},
})
expect(target).toStrictEqual({
expect(target.config).toMatchObject({
bin: 'foo',
cacheDir: '/path/to/cache/dir',
registry: 'https://example.com/local-registry/',
@@ -43,10 +51,10 @@ test('inheritAuthConfig copies only auth keys from source to target', () => {
registry: 'https://example.com/global-registry/',
'//example.com/global-registry/:_auth': 'MY_SECRET_GLOBAL_AUTH',
},
rawLocalConfig: {
bin: 'foo',
registry: 'https://example.com/local-registry/',
'//example.com/local-registry/:_authToken': 'MY_SECRET_LOCAL_AUTH',
},
})
expect(target.context.rawLocalConfig).toStrictEqual({
bin: 'foo',
registry: 'https://example.com/local-registry/',
'//example.com/local-registry/:_authToken': 'MY_SECRET_LOCAL_AUTH',
})
})

View File

@@ -1,5 +1,5 @@
import type { Config } from './Config.js'
import { type InheritableConfig, inheritPickedConfig } from './inheritPickedConfig.js'
import { type InheritableConfigPair, inheritPickedConfig } from './inheritPickedConfig.js'
import type { types } from './types.js'
const RAW_AUTH_CFG_KEYS = [
@@ -83,8 +83,8 @@ function pickAuthConfig (localCfg: Partial<Config>): Partial<Config> {
return result as Partial<Config>
}
export function inheritAuthConfig (targetCfg: InheritableConfig, authSrcCfg: InheritableConfig): void {
inheritPickedConfig(targetCfg, authSrcCfg, pickAuthConfig, pickRawAuthConfig)
export function inheritAuthConfig (target: InheritableConfigPair, src: InheritableConfigPair): void {
inheritPickedConfig(target, src, pickAuthConfig, pickRawAuthConfig)
}
/**

View File

@@ -27,6 +27,7 @@ import { checkGlobalBinDir } from './checkGlobalBinDir.js'
import { getDefaultWorkspaceConcurrency, getWorkspaceConcurrency } from './concurrency.js'
import type {
Config,
ConfigContext,
ConfigWithDeprecatedSettings,
ProjectConfig,
UniversalOptions,
@@ -63,7 +64,7 @@ export {
ProjectConfigUnsupportedFieldError,
} from './projectConfig.js'
export type { Config, ProjectConfig, UniversalOptions, VerifyDepsBeforeRun }
export type { Config, ConfigContext, ProjectConfig, UniversalOptions, VerifyDepsBeforeRun }
export { isIniConfigKey } from './auth.js'
export { type ConfigFileKey, isConfigFileKey } from './configFileKey.js'
@@ -89,7 +90,7 @@ export async function getConfig (opts: {
env?: Record<string, string | undefined>
ignoreNonAuthSettingsFromLocal?: boolean
ignoreLocalSettings?: boolean
}): Promise<{ config: Config, warnings: string[] }> {
}): Promise<{ config: Config, context: ConfigContext, warnings: string[] }> {
if (opts.ignoreNonAuthSettingsFromLocal) {
const { ignoreNonAuthSettingsFromLocal: _, ...authOpts } = opts
const globalCfgOpts: typeof authOpts = {
@@ -101,7 +102,7 @@ export async function getConfig (opts: {
},
}
const [final, authSrc] = await Promise.all([getConfig(globalCfgOpts), getConfig(authOpts)])
inheritAuthConfig(final.config, authSrc.config)
inheritAuthConfig(final, authSrc)
final.warnings.push(...authSrc.warnings)
return final
}
@@ -241,7 +242,7 @@ export async function getConfig (opts: {
const pnpmConfig = Object.fromEntries(
Object.entries(defaultOptions)
.map(([key, value]) => [camelcase(key, { locale: 'en-US' }), value])
) as unknown as ConfigWithDeprecatedSettings
) as unknown as (ConfigWithDeprecatedSettings & ConfigContext)
for (const [key, value] of Object.entries(npmrcResult.mergedConfig)) {
if (Object.hasOwn(types, key)) {
@@ -254,6 +255,7 @@ export async function getConfig (opts: {
// Track which keys are explicitly set (not defaults)
const explicitlySetKeys = new Set<string>(Object.keys(configFromCliOpts))
pnpmConfig.explicitlySetKeys = explicitlySetKeys
pnpmConfig.cliOptions = cliOptions
Object.assign(pnpmConfig, configFromCliOpts)
// Resolving the current working directory to its actual location is crucial.
@@ -377,8 +379,8 @@ export async function getConfig (opts: {
}
pnpmConfig.packageManager = packageManager
pnpmConfig.rootProjectManifestDir = pnpmConfig.lockfileDir ?? pnpmConfig.workspaceDir ?? pnpmConfig.dir
if (!opts.ignoreLocalSettings) {
pnpmConfig.rootProjectManifestDir = pnpmConfig.lockfileDir ?? pnpmConfig.workspaceDir ?? pnpmConfig.dir
pnpmConfig.rootProjectManifest = await safeReadProjectManifestOnly(pnpmConfig.rootProjectManifestDir) ?? undefined
if (pnpmConfig.rootProjectManifest != null) {
if (pnpmConfig.rootProjectManifest.workspaces?.length && !pnpmConfig.workspaceDir) {
@@ -622,7 +624,24 @@ export async function getConfig (opts: {
}
}
return { config: pnpmConfig, warnings }
const {
hooks, finders,
allProjects, selectedProjectsGraph, allProjectsGraph,
rootProjectManifest, rootProjectManifestDir,
cliOptions: ctxCliOptions, rawLocalConfig: ctxRawLocalConfig,
explicitlySetKeys: ctxExplicitlySetKeys,
packageManager: ctxPackageManager, wantedPackageManager,
...config
} = pnpmConfig as Config & ConfigContext
const context: ConfigContext = {
hooks, finders,
allProjects, selectedProjectsGraph, allProjectsGraph,
rootProjectManifest, rootProjectManifestDir,
cliOptions: ctxCliOptions, rawLocalConfig: ctxRawLocalConfig,
explicitlySetKeys: ctxExplicitlySetKeys,
packageManager: ctxPackageManager, wantedPackageManager,
}
return { config, context, warnings }
}
function getProcessEnv (env: string): string | undefined {
@@ -719,7 +738,7 @@ function getNodeVersionFromEnginesRuntime (manifest: ProjectManifest): string |
return undefined
}
function addSettingsFromWorkspaceManifestToConfig (pnpmConfig: Config, {
function addSettingsFromWorkspaceManifestToConfig (pnpmConfig: Config & ConfigContext, {
configFromCliOpts,
projectManifest,
workspaceManifest,

View File

@@ -1,17 +1,20 @@
import type { Config } from './Config.js'
import type { Config, ConfigContext } from './Config.js'
export type InheritableConfig = Partial<Config> & Pick<Config, 'authConfig' | 'rawLocalConfig'>
export interface InheritableConfigPair {
config: Partial<Config> & Pick<Config, 'authConfig'>
context: Pick<ConfigContext, 'rawLocalConfig'>
}
export type PickConfig = (cfg: Partial<Config>) => Partial<Config>
export type PickRawConfig = (cfg: Record<string, unknown>) => Record<string, unknown>
export function inheritPickedConfig (
targetCfg: InheritableConfig,
srcCfg: InheritableConfig,
target: InheritableConfigPair,
src: InheritableConfigPair,
pickConfig: PickConfig,
pickRawConfig: PickRawConfig,
pickRawLocalConfig: PickRawConfig = pickRawConfig
): void {
Object.assign(targetCfg, pickConfig(srcCfg))
Object.assign(targetCfg.authConfig, pickRawConfig(srcCfg.authConfig))
Object.assign(targetCfg.rawLocalConfig, pickRawLocalConfig(srcCfg.rawLocalConfig))
Object.assign(target.config, pickConfig(src.config))
Object.assign(target.config.authConfig, pickRawConfig(src.config.authConfig))
Object.assign(target.context.rawLocalConfig, pickRawLocalConfig(src.context.rawLocalConfig))
}

View File

@@ -678,7 +678,7 @@ test.skip('rawLocalConfig in a workspace', async () => {
fs.writeFileSync('.npmrc', 'hoist-pattern=eslint-*', 'utf8')
{
const { config } = await getConfig({
const { context } = await getConfig({
cliOptions: {
'save-exact': true,
},
@@ -689,7 +689,7 @@ test.skip('rawLocalConfig in a workspace', async () => {
workspaceDir,
})
expect(config.rawLocalConfig).toStrictEqual({
expect(context.rawLocalConfig).toStrictEqual({
'hoist-pattern': 'eslint-*',
'save-exact': true,
})
@@ -699,7 +699,7 @@ test.skip('rawLocalConfig in a workspace', async () => {
fs.mkdirSync('package2')
process.chdir('package2')
{
const { config } = await getConfig({
const { context } = await getConfig({
cliOptions: {
'save-exact': true,
},
@@ -710,7 +710,7 @@ test.skip('rawLocalConfig in a workspace', async () => {
workspaceDir,
})
expect(config.rawLocalConfig).toStrictEqual({
expect(context.rawLocalConfig).toStrictEqual({
'hoist-pattern': '*',
'save-exact': true,
})
@@ -722,7 +722,7 @@ test.skip('rawLocalConfig', async () => {
fs.writeFileSync('.npmrc', 'modules-dir=modules', 'utf8')
const { config } = await getConfig({
const { context } = await getConfig({
cliOptions: {
'save-exact': true,
},
@@ -732,7 +732,7 @@ test.skip('rawLocalConfig', async () => {
},
})
expect(config.rawLocalConfig).toStrictEqual({
expect(context.rawLocalConfig).toStrictEqual({
'modules-dir': 'modules',
'save-exact': true,
})

View File

@@ -1,5 +1,5 @@
import { docsUrl, TABLE_OPTIONS } from '@pnpm/cli.utils'
import { type Config, types as allTypes, type UniversalOptions } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes, type UniversalOptions } from '@pnpm/config.reader'
import { WANTED_LOCKFILE } from '@pnpm/constants'
import { audit, type AuditAdvisory, type AuditLevelNumber, type AuditLevelString, type AuditReport, type AuditVulnerabilityCounts, type IgnoredAuditVulnerabilityCounts } from '@pnpm/deps.compliance.audit'
import { PnpmError } from '@pnpm/error'
@@ -166,10 +166,11 @@ export type AuditOptions = Pick<UniversalOptions, 'dir'> & {
| 'optional'
| 'userConfig'
| 'authConfig'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'virtualStoreDirMaxLength'
| 'workspaceDir'
> & Pick<ConfigContext,
| 'rootProjectManifest'
| 'rootProjectManifestDir'
> & InstallCommandOptions
const DEFAULT_FIX_METHOD = 'override'

View File

@@ -1,7 +1,7 @@
import path from 'node:path'
import { readProjectManifestOnly } from '@pnpm/cli.utils'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { WANTED_LOCKFILE } from '@pnpm/constants'
import { findDependencyLicenses } from '@pnpm/deps.compliance.license-scanner'
import { PnpmError } from '@pnpm/error'
@@ -28,11 +28,12 @@ export type LicensesCommandOptions = {
| 'virtualStoreDir'
| 'modulesDir'
| 'pnpmHomeDir'
| 'supportedArchitectures'
| 'virtualStoreDirMaxLength'
> & Pick<ConfigContext,
| 'selectedProjectsGraph'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'supportedArchitectures'
| 'virtualStoreDirMaxLength'
> &
Partial<Pick<Config, 'userConfig'>>

View File

@@ -1,7 +1,7 @@
import { FILTERING } from '@pnpm/cli.common-cli-options-help'
import { packageManager } from '@pnpm/cli.meta'
import { docsUrl, readProjectManifestOnly } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { WANTED_LOCKFILE } from '@pnpm/constants'
import {
collectSbomComponents,
@@ -34,10 +34,11 @@ export type SbomCommandOptions = {
| 'virtualStoreDir'
| 'modulesDir'
| 'pnpmHomeDir'
| 'selectedProjectsGraph'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'virtualStoreDirMaxLength'
> & Pick<ConfigContext,
| 'selectedProjectsGraph'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
> &
Partial<Pick<Config, 'userConfig'>>

View File

@@ -1,6 +1,6 @@
import { FILTERING, OPTIONS, UNIVERSAL_OPTIONS } from '@pnpm/cli.common-cli-options-help'
import { docsUrl } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { list, listForPackages } from '@pnpm/deps.inspection.list'
import { listGlobalPackages } from '@pnpm/global.commands'
import type { Finder, IncludedDependencies } from '@pnpm/types'
@@ -84,16 +84,17 @@ For example: pnpm ls babel-* eslint-*',
}
export type ListCommandOptions = Pick<Config,
| 'allProjects'
| 'dev'
| 'dir'
| 'finders'
| 'optional'
| 'production'
| 'selectedProjectsGraph'
| 'modulesDir'
| 'virtualStoreDirMaxLength'
> & Partial<Pick<Config, 'cliOptions'>> & {
> & Pick<ConfigContext,
| 'allProjects'
| 'finders'
| 'selectedProjectsGraph'
> & Partial<Pick<ConfigContext, 'cliOptions'>> & {
alwaysPrintRootPackage?: boolean
depth?: number
excludePeers?: boolean

View File

@@ -9,7 +9,7 @@ import {
TABLE_OPTIONS,
} from '@pnpm/cli.utils'
import colorizeSemverDiff from '@pnpm/colorize-semver-diff'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import {
outdatedDepsOfProjects,
type OutdatedPackage,
@@ -139,7 +139,6 @@ export type OutdatedCommandOptions = {
format?: 'table' | 'list' | 'json'
sortBy?: 'name'
} & Pick<Config,
| 'allProjects'
| 'ca'
| 'cacheDir'
| 'catalogs'
@@ -167,11 +166,13 @@ export type OutdatedCommandOptions = {
| 'production'
| 'authConfig'
| 'registries'
| 'selectedProjectsGraph'
| 'strictSsl'
| 'tag'
| 'userAgent'
| 'updateConfig'
> & Pick<ConfigContext,
| 'allProjects'
| 'selectedProjectsGraph'
> & Partial<Pick<Config, 'globalPkgDir' | 'userConfig'>>
export async function handler (

View File

@@ -1,6 +1,6 @@
import { FILTERING, UNIVERSAL_OPTIONS } from '@pnpm/cli.common-cli-options-help'
import { docsUrl } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { checkPeerDependencies } from '@pnpm/deps.inspection.peers-checker'
import type { PeerDependencyIssuesByProjects } from '@pnpm/types'
import chalk from 'chalk'
@@ -66,8 +66,8 @@ export type PeersCommandOptions = Pick<Config,
| 'dir'
| 'modulesDir'
| 'peerDependencyRules'
| 'selectedProjectsGraph'
> & Partial<Pick<Config, 'cliOptions'>> & {
> & Pick<ConfigContext, 'selectedProjectsGraph'>
& Partial<Pick<ConfigContext, 'cliOptions'>> & {
json?: boolean
lockfileDir?: string
lockfileOnly?: boolean

View File

@@ -1,5 +1,5 @@
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { PnpmError } from '@pnpm/error'
import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header'
import { createFetchFromRegistry } from '@pnpm/network.fetch'
@@ -49,7 +49,7 @@ export function help (): string {
}
export async function handler (
opts: Config & {
opts: Config & ConfigContext & {
json?: boolean
},
params: string[]

View File

@@ -1,4 +1,4 @@
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { view } from '@pnpm/deps.inspection.commands'
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
@@ -37,79 +37,79 @@ test('view: rcOptionsTypes should return object', () => {
test('view: missing package name throws error', async () => {
await expect(
view.handler(VIEW_OPTIONS as unknown as Config, [])
view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, [])
).rejects.toMatchObject({ code: 'ERR_PNPM_MISSING_PACKAGE_NAME' })
})
test('view: non-registry spec throws error', async () => {
await expect(
view.handler(VIEW_OPTIONS as unknown as Config, ['github:user/repo'])
view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['github:user/repo'])
).rejects.toMatchObject({ code: 'ERR_PNPM_INVALID_PACKAGE_NAME' })
})
test('view: successful lookup of package', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative'])
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative'])
expect(typeof result).toBe('string')
expect(result).toContain('is-negative')
})
test('view: package not found throws an error', async () => {
await expect(
view.handler(VIEW_OPTIONS as unknown as Config, ['not-a-real-package-123456789'])
view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['not-a-real-package-123456789'])
).rejects.toMatchObject({ code: 'ERR_PNPM_FETCH_404' })
})
test('view: no matching version throws an error', async () => {
await expect(
view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative@99999.0.0'])
view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative@99999.0.0'])
).rejects.toMatchObject({ code: 'ERR_PNPM_PACKAGE_NOT_FOUND' })
})
test('view: with --json option', async () => {
const result = await view.handler({ ...VIEW_OPTIONS, json: true } as unknown as Config, ['is-negative'])
const result = await view.handler({ ...VIEW_OPTIONS, json: true } as unknown as Config & ConfigContext, ['is-negative'])
expect(typeof result).toBe('string')
const parsed = JSON.parse(result as string)
expect(parsed.name).toBe('is-negative')
})
test('view: accessing a specific field', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative', 'name'])
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative', 'name'])
expect(result).toBe('is-negative')
})
test('view: accessing a specific version', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative@1.0.0', 'version'])
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative@1.0.0', 'version'])
expect(result).toBe('1.0.0')
})
test('view: accessing multiple fields adds quotes for strings', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative@1.0.0', 'name', 'version'])
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative@1.0.0', 'name', 'version'])
expect(typeof result).toBe('string')
expect(result).toContain("name = 'is-negative'")
expect(result).toContain("version = '1.0.0'")
})
test('view: version range resolves to matching version', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative@^1.0.0', 'version'])
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative@^1.0.0', 'version'])
expect(typeof result).toBe('string')
expect(result).toMatch(/^1\./)
})
test('view: dist-tag resolves correctly', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative@latest', 'version'])
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative@latest', 'version'])
expect(typeof result).toBe('string')
expect(result).toMatch(/^\d+\.\d+\.\d+/)
})
test('view: nested field selection', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative@1.0.0', 'dist.shasum'])
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative@1.0.0', 'dist.shasum'])
expect(typeof result).toBe('string')
expect(result!.length).toBeGreaterThan(0)
})
test('view: field selection with --json', async () => {
const result = await view.handler(
{ ...VIEW_OPTIONS, json: true } as unknown as Config,
{ ...VIEW_OPTIONS, json: true } as unknown as Config & ConfigContext,
['is-negative@1.0.0', 'name', 'version']
)
const parsed = JSON.parse(result as string)
@@ -118,43 +118,43 @@ test('view: field selection with --json', async () => {
})
test('view: text output includes header with name@version', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative@1.0.0']) as string
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative@1.0.0']) as string
const firstLine = result.split('\n')[0]
expect(firstLine).toContain('is-negative@1.0.0')
})
test('view: text output includes dist section', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative@1.0.0']) as string
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative@1.0.0']) as string
expect(result).toContain('.tarball:')
expect(result).toContain('.shasum:')
})
test('view: text output includes dist-tags', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative']) as string
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative']) as string
expect(result).toContain('dist-tags:')
expect(result).toContain('latest:')
})
test('view: text output for package with dependencies shows deps count', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['@pnpm.e2e/pkg-with-1-dep@100.0.0']) as string
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['@pnpm.e2e/pkg-with-1-dep@100.0.0']) as string
const firstLine = result.split('\n')[0]
expect(firstLine).toContain('deps: ')
expect(firstLine).not.toContain('deps: none')
})
test('view: text output for package without dependencies shows deps: none', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative@1.0.0']) as string
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative@1.0.0']) as string
const firstLine = result.split('\n')[0]
expect(firstLine).toContain('deps: none')
})
test('view: scoped package lookup', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['@pnpm.e2e/pkg-with-1-dep@100.0.0', 'name'])
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['@pnpm.e2e/pkg-with-1-dep@100.0.0', 'name'])
expect(result).toBe('@pnpm.e2e/pkg-with-1-dep')
})
test('view: object field renders as JSON', async () => {
const result = await view.handler(VIEW_OPTIONS as unknown as Config, ['is-negative@1.0.0', 'dist'])
const result = await view.handler(VIEW_OPTIONS as unknown as Config & ConfigContext, ['is-negative@1.0.0', 'dist'])
expect(typeof result).toBe('string')
const parsed = JSON.parse(result as string)
expect(parsed.tarball).toBeDefined()

View File

@@ -3,7 +3,7 @@ import path from 'node:path'
import util from 'node:util'
import { parseOverrides } from '@pnpm/config.parse-overrides'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { MANIFEST_BASE_NAMES, WANTED_LOCKFILE } from '@pnpm/constants'
import { hashObjectNullableWithPrefix } from '@pnpm/crypto.object-hasher'
import { PnpmError } from '@pnpm/error'
@@ -42,18 +42,14 @@ import { safeStat, safeStatSync } from './safeStat.js'
import { statManifestFile } from './statManifestFile.js'
export type CheckDepsStatusOptions = Pick<Config,
| 'allProjects'
| 'autoInstallPeers'
| 'catalogs'
| 'excludeLinksFromLockfile'
| 'injectWorkspacePackages'
| 'linkWorkspacePackages'
| 'nodeLinker'
| 'hooks'
| 'patchedDependencies'
| 'peersSuffixMaxLength'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'sharedWorkspaceLockfile'
| 'workspaceDir'
| 'patchesDir'
@@ -61,6 +57,11 @@ export type CheckDepsStatusOptions = Pick<Config,
| 'overrides'
| 'packageExtensions'
| 'ignoredOptionalDependencies'
> & Pick<ConfigContext,
| 'allProjects'
| 'hooks'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
> & {
ignoreFilteredInstallCache?: boolean
ignoredWorkspaceStateSettings?: Array<keyof WorkspaceStateSettings>

View File

@@ -3,7 +3,7 @@ import path from 'node:path'
import { linkBins } from '@pnpm/bins.linker'
import { isExecutedByCorepack, packageManager } from '@pnpm/cli.meta'
import { docsUrl } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { PnpmError } from '@pnpm/error'
import { createResolver } from '@pnpm/installing.client'
import { resolvePackageManagerIntegrities } from '@pnpm/installing.env-installer'
@@ -52,6 +52,7 @@ export type SelfUpdateCommandOptions = CreateStoreControllerOptions & Pick<Confi
| 'managePackageManagerVersions'
| 'modulesDir'
| 'pnpmHomeDir'
> & Pick<ConfigContext,
| 'rootProjectManifestDir'
| 'wantedPackageManager'
>

View File

@@ -1,4 +1,4 @@
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
export type NvmNodeCommandOptions = Pick<Config,
| 'bin'
@@ -24,11 +24,9 @@ export type NvmNodeCommandOptions = Pick<Config,
> & Partial<Pick<Config,
| 'cacheDir'
| 'configDir'
| 'cliOptions'
| 'sslConfigs'
// Fields needed to forward opts to add.handler for env use
| 'registries'
| 'rawLocalConfig'
| 'lockfileDir'
| 'nodeLinker'
| 'modulesDir'
@@ -38,6 +36,9 @@ export type NvmNodeCommandOptions = Pick<Config,
| 'sideEffectsCache'
| 'sideEffectsCacheReadonly'
| 'supportedArchitectures'
>> & Partial<Pick<ConfigContext,
| 'cliOptions'
| 'rawLocalConfig'
>> & {
remote?: boolean
}

View File

@@ -2,7 +2,7 @@ import path from 'node:path'
import { FILTERING, UNIVERSAL_OPTIONS } from '@pnpm/cli.common-cli-options-help'
import { docsUrl, readProjectManifestOnly, type RecursiveSummary, throwOnCommandFail } from '@pnpm/cli.utils'
import { type Config, getWorkspaceConcurrency, types } from '@pnpm/config.reader'
import { type Config, type ConfigContext, getWorkspaceConcurrency, types } from '@pnpm/config.reader'
import { lifecycleLogger, type LifecycleMessage } from '@pnpm/core-loggers'
import type { CheckDepsStatusOptions } from '@pnpm/deps.status'
import { PnpmError } from '@pnpm/error'
@@ -136,7 +136,7 @@ export function getExecutionDuration (start: [number, number]): number {
return (end[0] * 1e9 + end[1]) / 1e6
}
export type ExecOpts = Required<Pick<Config, 'selectedProjectsGraph'>> & {
export type ExecOpts = Required<Pick<ConfigContext, 'selectedProjectsGraph'>> & {
bail?: boolean
unsafePerm?: boolean
reverse?: boolean
@@ -148,7 +148,6 @@ export type ExecOpts = Required<Pick<Config, 'selectedProjectsGraph'>> & {
implicitlyFellbackFromRun?: boolean
} & Pick<Config,
| 'bin'
| 'cliOptions'
| 'dir'
| 'extraBinPaths'
| 'extraEnv'
@@ -161,7 +160,7 @@ export type ExecOpts = Required<Pick<Config, 'selectedProjectsGraph'>> & {
| 'userAgent'
| 'verifyDepsBeforeRun'
| 'workspaceDir'
> & CheckDepsStatusOptions
> & Pick<ConfigContext, 'cliOptions'> & CheckDepsStatusOptions
export async function handler (
opts: ExecOpts,

View File

@@ -7,7 +7,7 @@ import {
readProjectManifestOnly,
tryReadProjectManifest,
} from '@pnpm/cli.utils'
import { type Config, getWorkspaceConcurrency, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, getWorkspaceConcurrency, types as allTypes } from '@pnpm/config.reader'
import type { CheckDepsStatusOptions } from '@pnpm/deps.status'
import { PnpmError } from '@pnpm/error'
import {
@@ -161,7 +161,6 @@ export type RunOpts =
& { recursive?: boolean }
& Pick<Config,
| 'bin'
| 'cliOptions'
| 'verifyDepsBeforeRun'
| 'dir'
| 'enablePrePostScripts'
@@ -177,9 +176,10 @@ export type RunOpts =
| 'syncInjectedDepsAfterScripts'
| 'userAgent'
>
& Pick<ConfigContext, 'cliOptions'>
& (
| { recursive?: false } & Partial<Pick<Config, 'allProjects' | 'selectedProjectsGraph' | 'workspaceDir'>>
| { recursive: true } & Required<Pick<Config, 'allProjects' | 'selectedProjectsGraph' | 'workspaceDir'>>
| { recursive?: false } & Partial<Pick<ConfigContext, 'allProjects' | 'selectedProjectsGraph'> & Pick<Config, 'workspaceDir'>>
| { recursive: true } & Required<Pick<ConfigContext, 'allProjects' | 'selectedProjectsGraph'> & Pick<Config, 'workspaceDir'>>
)
& {
argv?: {

View File

@@ -3,7 +3,7 @@ import path from 'node:path'
import util from 'node:util'
import { throwOnCommandFail } from '@pnpm/cli.utils'
import { type Config, getWorkspaceConcurrency } from '@pnpm/config.reader'
import { type Config, type ConfigContext, getWorkspaceConcurrency } from '@pnpm/config.reader'
import { PnpmError } from '@pnpm/error'
import {
makeNodeRequireOption,
@@ -28,14 +28,13 @@ export type RecursiveRunOpts = Pick<Config,
| 'pnpmHomeDir'
| 'requiredScripts'
| 'userAgent'
| 'rootProjectManifest'
| 'scriptsPrependNodePath'
| 'scriptShell'
| 'shellEmulator'
| 'stream'
| 'syncInjectedDepsAfterScripts'
| 'workspaceDir'
> & Required<Pick<Config, 'allProjects' | 'selectedProjectsGraph' | 'workspaceDir' | 'dir'>> &
> & Pick<ConfigContext, 'rootProjectManifest'> & Required<Pick<ConfigContext, 'allProjects' | 'selectedProjectsGraph'> & Pick<Config, 'workspaceDir' | 'dir'>> &
Partial<Pick<Config, 'extraBinPaths' | 'extraEnv' | 'bail' | 'reporter' | 'reverse' | 'sort' | 'workspaceConcurrency'>> &
{
ifPresent?: boolean

View File

@@ -1,6 +1,6 @@
import { UNIVERSAL_OPTIONS } from '@pnpm/cli.common-cli-options-help'
import { docsUrl } from '@pnpm/cli.utils'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { type InstallOptions, mutateModulesInSingleProject } from '@pnpm/installing.deps-installer'
import { createStoreController, type CreateStoreControllerOptions } from '@pnpm/store.connection-manager'
import type { ProjectRootDir } from '@pnpm/types'
@@ -46,7 +46,7 @@ export function help (): string {
})
}
type FetchCommandOptions = Pick<Config, 'production' | 'dev' | 'enableGlobalVirtualStore' | 'patchedDependencies' | 'rootProjectManifest' | 'rootProjectManifestDir'> & CreateStoreControllerOptions
type FetchCommandOptions = Pick<Config, 'production' | 'dev' | 'enableGlobalVirtualStore' | 'patchedDependencies'> & Pick<ConfigContext, 'rootProjectManifest' | 'rootProjectManifestDir'> & CreateStoreControllerOptions
export async function handler (opts: FetchCommandOptions): Promise<void> {
const store = await createStoreController(opts)

View File

@@ -2,7 +2,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { docsUrl } from '@pnpm/cli.utils'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { WANTED_LOCKFILE } from '@pnpm/constants'
import { PnpmError } from '@pnpm/error'
import gfs from '@pnpm/fs.graceful-fs'
@@ -99,14 +99,15 @@ export const commandNames = ['import']
export const recursiveByDefault = true
export type ImportCommandOptions = Pick<Config,
| 'allProjects'
| 'allProjectsGraph'
| 'selectedProjectsGraph'
| 'workspaceDir'
| 'ignoreWorkspaceCycles'
| 'disallowWorkspaceCycles'
| 'sharedWorkspaceLockfile'
| 'workspacePackagePatterns'
> & Pick<ConfigContext,
| 'allProjects'
| 'allProjectsGraph'
| 'selectedProjectsGraph'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
> & CreateStoreControllerOptions & Omit<InstallOptions, 'storeController' | 'lockfileOnly' | 'preferredVersions'>

View File

@@ -1,7 +1,7 @@
import type { CommandHandlerMap } from '@pnpm/cli.command'
import { FILTERING, OPTIONS, OUTPUT_OPTIONS, UNIVERSAL_OPTIONS } from '@pnpm/cli.common-cli-options-help'
import { docsUrl } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { WANTED_LOCKFILE } from '@pnpm/constants'
import { PnpmError } from '@pnpm/error'
import type { CreateStoreControllerOptions } from '@pnpm/store.connection-manager'
@@ -265,12 +265,10 @@ Install all optionalDependencies even when they don\'t satisfy the current envir
}
export type InstallCommandOptions = Pick<Config,
| 'allProjects'
| 'autoInstallPeers'
| 'bail'
| 'bin'
| 'catalogs'
| 'cliOptions'
| 'configDependencies'
| 'dedupeInjectedDeps'
| 'dedupeDirectDeps'
@@ -285,12 +283,10 @@ export type InstallCommandOptions = Pick<Config,
| 'frozenLockfile'
| 'global'
| 'globalPnpmfile'
| 'hooks'
| 'ignorePnpmfile'
| 'ignoreScripts'
| 'injectWorkspacePackages'
| 'linkWorkspacePackages'
| 'rawLocalConfig'
| 'lockfileDir'
| 'lockfileOnly'
| 'modulesDir'
@@ -300,8 +296,6 @@ export type InstallCommandOptions = Pick<Config,
| 'preferWorkspacePackages'
| 'production'
| 'registries'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'save'
| 'saveDev'
| 'saveExact'
@@ -312,8 +306,6 @@ export type InstallCommandOptions = Pick<Config,
| 'saveCatalogName'
| 'saveWorkspaceProtocol'
| 'lockfileIncludeTarballUrl'
| 'allProjectsGraph'
| 'selectedProjectsGraph'
| 'sideEffectsCache'
| 'sideEffectsCacheReadonly'
| 'sort'
@@ -334,6 +326,15 @@ export type InstallCommandOptions = Pick<Config,
| 'packageExtensions'
| 'supportedArchitectures'
| 'packageConfigs'
> & Pick<ConfigContext,
| 'allProjects'
| 'cliOptions'
| 'hooks'
| 'rawLocalConfig'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'allProjectsGraph'
| 'selectedProjectsGraph'
> & CreateStoreControllerOptions & Partial<Pick<Config, 'globalPkgDir'>> & {
argv: {
original: string[]

View File

@@ -6,7 +6,7 @@ import {
readProjectManifestOnly,
tryReadProjectManifest,
} from '@pnpm/cli.utils'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { checkDepsStatus } from '@pnpm/deps.status'
import { PnpmError } from '@pnpm/error'
import { arrayOfWorkspacePackagesToMap } from '@pnpm/installing.context'
@@ -57,15 +57,12 @@ const OVERWRITE_UPDATE_OPTIONS = {
}
export type InstallDepsOptions = Pick<Config,
| 'allProjects'
| 'allProjectsGraph'
| 'autoInstallPeers'
| 'bail'
| 'bin'
| 'catalogs'
| 'catalogMode'
| 'cleanupUnusedCatalogs'
| 'cliOptions'
| 'dedupePeerDependents'
| 'dedupePeers'
| 'depth'
@@ -76,7 +73,6 @@ export type InstallDepsOptions = Pick<Config,
| 'excludeLinksFromLockfile'
| 'global'
| 'globalPnpmfile'
| 'hooks'
| 'ignoreCurrentSpecifiers'
| 'ignorePnpmfile'
| 'ignoreScripts'
@@ -86,10 +82,7 @@ export type InstallDepsOptions = Pick<Config,
| 'lockfileOnly'
| 'production'
| 'preferWorkspacePackages'
| 'rawLocalConfig'
| 'registries'
| 'rootProjectManifestDir'
| 'rootProjectManifest'
| 'save'
| 'saveDev'
| 'saveExact'
@@ -101,7 +94,6 @@ export type InstallDepsOptions = Pick<Config,
| 'lockfileIncludeTarballUrl'
| 'scriptsPrependNodePath'
| 'scriptShell'
| 'selectedProjectsGraph'
| 'sideEffectsCache'
| 'sideEffectsCacheReadonly'
| 'sort'
@@ -119,6 +111,15 @@ export type InstallDepsOptions = Pick<Config,
| 'configDependencies'
| 'packageExtensions'
| 'updateConfig'
> & Pick<ConfigContext,
| 'allProjects'
| 'allProjectsGraph'
| 'cliOptions'
| 'hooks'
| 'rawLocalConfig'
| 'rootProjectManifestDir'
| 'rootProjectManifest'
| 'selectedProjectsGraph'
> & CreateStoreControllerOptions & {
argv: {
original: string[]

View File

@@ -5,7 +5,7 @@ import {
docsUrl,
tryReadProjectManifest,
} from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { writeSettings } from '@pnpm/config.writer'
import { PnpmError } from '@pnpm/error'
import { arrayOfWorkspacePackagesToMap } from '@pnpm/installing.context'
@@ -29,10 +29,7 @@ const isFilespec = isWindows ? /^(?:[./\\]|~\/|[a-z]:)/i : /^(?:[./]|~\/|[a-z]:)
type LinkOpts = Pick<Config,
| 'bin'
| 'cliOptions'
| 'engineStrict'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'overrides'
| 'saveDev'
| 'saveOptional'
@@ -40,6 +37,10 @@ type LinkOpts = Pick<Config,
| 'workspaceDir'
| 'workspacePackagePatterns'
| 'sharedWorkspaceLockfile'
> & Pick<ConfigContext,
| 'cliOptions'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
> & Partial<Pick<Config, 'linkWorkspacePackages'>> & install.InstallCommandOptions
export const rcOptionsTypes = cliOptionsTypes

View File

@@ -10,6 +10,7 @@ import {
import { createMatcherWithIndex } from '@pnpm/config.matcher'
import {
type Config,
type ConfigContext,
createProjectConfigRecord,
getWorkspaceConcurrency,
type OptionsFromRootManifest,
@@ -63,7 +64,6 @@ export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
| 'depth'
| 'globalPnpmfile'
| 'hoistPattern'
| 'hooks'
| 'ignorePnpmfile'
| 'ignoreScripts'
| 'linkWorkspacePackages'
@@ -71,10 +71,7 @@ export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
| 'lockfileOnly'
| 'modulesDir'
| 'allowBuilds'
| 'rawLocalConfig'
| 'registries'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'save'
| 'saveCatalogName'
| 'saveDev'
@@ -90,6 +87,11 @@ export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
| 'cleanupUnusedCatalogs'
| 'packageConfigs'
| 'updateConfig'
> & Pick<ConfigContext,
| 'hooks'
| 'rawLocalConfig'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
> & {
rebuildHandler?: CommandHandler
include?: IncludedDependencies

View File

@@ -5,7 +5,7 @@ import {
readDepNameCompletions,
readProjectManifest,
} from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { PnpmError } from '@pnpm/error'
import { handleGlobalRemove } from '@pnpm/global.commands'
import { arrayOfWorkspacePackagesToMap } from '@pnpm/installing.context'
@@ -126,32 +126,33 @@ export const completion: CompletionFunc = async (cliOpts) => {
export async function handler (
opts: CreateStoreControllerOptions & Pick<Config,
| 'allProjects'
| 'allProjectsGraph'
| 'bail'
| 'bin'
| 'configDependencies'
| 'dev'
| 'engineStrict'
| 'globalPnpmfile'
| 'hooks'
| 'ignorePnpmfile'
| 'linkWorkspacePackages'
| 'lockfileDir'
| 'optional'
| 'production'
| 'rawLocalConfig'
| 'registries'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'saveDev'
| 'saveOptional'
| 'saveProd'
| 'selectedProjectsGraph'
| 'workspaceDir'
| 'workspacePackagePatterns'
| 'sharedWorkspaceLockfile'
| 'cleanupUnusedCatalogs'
> & Pick<ConfigContext,
| 'allProjects'
| 'allProjectsGraph'
| 'hooks'
| 'rawLocalConfig'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'selectedProjectsGraph'
> & {
recursive?: boolean
pnpmfile: string[]

View File

@@ -2,7 +2,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { docsUrl } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { PnpmError } from '@pnpm/error'
import type { LogBase } from '@pnpm/logger'
import { applyPatchToDir } from '@pnpm/patching.apply-patch'
@@ -63,11 +63,12 @@ export type PatchCommandOptions = Pick<Config,
| 'registries'
| 'tag'
| 'storeDir'
| 'rootProjectManifest'
| 'lockfileDir'
| 'modulesDir'
| 'virtualStoreDir'
| 'sharedWorkspaceLockfile'
> & Pick<ConfigContext,
| 'rootProjectManifest'
> & CreateStoreControllerOptions & {
editDir?: string
reporter?: (logObj: LogBase) => void

View File

@@ -2,7 +2,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { docsUrl } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { createShortHash } from '@pnpm/crypto.hash'
import { PnpmError } from '@pnpm/error'
import { packlist } from '@pnpm/fs.packlist'
@@ -52,7 +52,7 @@ export function help (): string {
})
}
type PatchCommitCommandOptions = install.InstallCommandOptions & Pick<Config, 'patchesDir' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'patchedDependencies'>
type PatchCommitCommandOptions = install.InstallCommandOptions & Pick<Config, 'patchesDir' | 'patchedDependencies'> & Pick<ConfigContext, 'rootProjectManifest' | 'rootProjectManifestDir'>
export async function handler (opts: PatchCommitCommandOptions, params: string[]): Promise<string | undefined> {
const userDir = params[0]

View File

@@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import { docsUrl } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { PnpmError } from '@pnpm/error'
import { install } from '@pnpm/installing.commands'
import enquirer from 'enquirer'
@@ -31,7 +31,7 @@ export function help (): string {
})
}
export type PatchRemoveCommandOptions = install.InstallCommandOptions & Pick<Config, 'dir' | 'lockfileDir' | 'patchesDir' | 'rootProjectManifest' | 'patchedDependencies'>
export type PatchRemoveCommandOptions = install.InstallCommandOptions & Pick<Config, 'dir' | 'lockfileDir' | 'patchesDir' | 'patchedDependencies'> & Pick<ConfigContext, 'rootProjectManifest'>
export async function handler (opts: PatchRemoveCommandOptions, params: string[]): Promise<void> {
let patchesToRemove = params

View File

@@ -3,7 +3,7 @@ import path from 'node:path'
import { formatWarn } from '@pnpm/cli.default-reporter'
import { packageManager } from '@pnpm/cli.meta'
import { type CliOptions, type Config, getConfig as _getConfig } from '@pnpm/config.reader'
import { type CliOptions, type Config, type ConfigContext, getConfig as _getConfig } from '@pnpm/config.reader'
import { requireHooks } from '@pnpm/hooks.pnpmfile'
import { resolveAndInstallConfigDeps } from '@pnpm/installing.env-installer'
import { createStoreController } from '@pnpm/store.connection-manager'
@@ -18,15 +18,15 @@ export async function getConfig (
workspaceDir: string | undefined
ignoreNonAuthSettingsFromLocal?: boolean
}
): Promise<Config> {
let { config, warnings } = await _getConfig({
): Promise<{ config: Config, context: ConfigContext }> {
const { config, context, warnings } = await _getConfig({
cliOptions,
globalDirShouldAllowWrite: opts.globalDirShouldAllowWrite,
packageManager,
workspaceDir: opts.workspaceDir,
ignoreNonAuthSettingsFromLocal: opts.ignoreNonAuthSettingsFromLocal,
})
config.cliOptions = cliOptions
context.cliOptions = cliOptions
applyDerivedConfig(config)
if (opts.excludeReporter) {
@@ -37,18 +37,22 @@ export async function getConfig (
console.warn(warnings.map((warning) => formatWarn(warning)).join('\n'))
}
return config
return { config, context }
}
export async function installConfigDepsAndLoadHooks (config: Config): Promise<Config> {
export async function installConfigDepsAndLoadHooks (
config: Config,
context: ConfigContext
): Promise<{ config: Config, context: ConfigContext }> {
if (config.configDependencies) {
const store = await createStoreController(config)
const store = await createStoreController({ ...config, ...context })
try {
await resolveAndInstallConfigDeps(config.configDependencies, {
...config,
...context,
store: store.ctrl,
storeDir: store.dir,
rootDir: config.lockfileDir ?? config.rootProjectManifestDir,
rootDir: config.lockfileDir ?? context.rootProjectManifestDir,
frozenLockfile: config.frozenLockfile,
})
} finally {
@@ -59,7 +63,7 @@ export async function installConfigDepsAndLoadHooks (config: Config): Promise<Co
config.tryLoadDefaultPnpmfile = config.pnpmfile == null
const pnpmfiles = config.pnpmfile == null ? [] : Array.isArray(config.pnpmfile) ? config.pnpmfile : [config.pnpmfile]
if (config.configDependencies) {
const configModulesDir = path.join(config.lockfileDir ?? config.rootProjectManifestDir, 'node_modules/.pnpm-config')
const configModulesDir = path.join(config.lockfileDir ?? context.rootProjectManifestDir, 'node_modules/.pnpm-config')
pnpmfiles.unshift(...calcPnpmfilePathsOfPluginDeps(configModulesDir, config.configDependencies))
}
const { hooks, finders, resolvedPnpmfilePaths } = await requireHooks(config.lockfileDir ?? config.dir, {
@@ -67,17 +71,17 @@ export async function installConfigDepsAndLoadHooks (config: Config): Promise<Co
pnpmfiles,
tryLoadDefaultPnpmfile: config.tryLoadDefaultPnpmfile,
})
config.hooks = hooks
config.finders = finders
context.hooks = hooks
context.finders = finders
config.pnpmfile = resolvedPnpmfilePaths
if (config.hooks?.updateConfig) {
for (const updateConfig of config.hooks.updateConfig) {
if (context.hooks?.updateConfig) {
for (const updateConfig of context.hooks.updateConfig) {
const updateConfigResult = updateConfig(config)
config = updateConfigResult instanceof Promise ? await updateConfigResult : updateConfigResult // eslint-disable-line no-await-in-loop
}
}
}
return config
return { config, context }
}
export function * calcPnpmfilePathsOfPluginDeps (configModulesDir: string, configDependencies: ConfigDependencies): Generator<string> {

View File

@@ -10,7 +10,7 @@ import path from 'node:path'
import { stripVTControlCharacters as stripAnsi } from 'node:util'
import { isExecutedByCorepack, packageManager } from '@pnpm/cli.meta'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { executionTimeLogger, scopeLogger } from '@pnpm/core-loggers'
import { PnpmError } from '@pnpm/error'
import { globalWarn, logger } from '@pnpm/logger'
@@ -87,6 +87,7 @@ export async function main (inputArgv: string[]): Promise<void> {
parseable?: boolean
json?: boolean
}
let context: ConfigContext
try {
// When we just want to print the location of the global bin directory,
// we don't need the write permission to it. Related issue: #2700
@@ -95,16 +96,16 @@ export async function main (inputArgv: string[]): Promise<void> {
if (cmd === 'link' && cliParams.length === 0) {
cliOptions.global = true
}
config = await getConfig(cliOptions, {
;({ config, context } = await getConfig(cliOptions, {
excludeReporter: false,
globalDirShouldAllowWrite,
workspaceDir,
ignoreNonAuthSettingsFromLocal: isDlxOrCreateCommand,
}) as typeof config
if (!isExecutedByCorepack() && cmd !== 'setup' && config.wantedPackageManager != null) {
const pm = config.wantedPackageManager
}) as { config: typeof config, context: ConfigContext })
if (!isExecutedByCorepack() && cmd !== 'setup' && context.wantedPackageManager != null) {
const pm = context.wantedPackageManager
if (pm.onFail === 'download' && pm.name === 'pnpm' && cmd !== 'self-update') {
await switchCliVersion(config)
await switchCliVersion(config, context)
} else if (pm.onFail !== 'ignore' && (!cmd || !skipPackageManagerCheckForCommand.has(cmd))) {
if (cliOptions.global) {
globalWarn('Using --global skips the package manager check for this project')
@@ -113,7 +114,7 @@ export async function main (inputArgv: string[]): Promise<void> {
}
}
}
config = await installConfigDepsAndLoadHooks(config) as typeof config
;({ config, context } = await installConfigDepsAndLoadHooks(config, context) as { config: typeof config, context: ConfigContext })
if (isDlxOrCreateCommand || cmd === 'sbom') {
config.useStderr = true
}
@@ -167,7 +168,7 @@ export async function main (inputArgv: string[]): Promise<void> {
if (printLogs) {
initReporter(reporterType, {
cmd,
config,
config: { ...config, ...context },
})
global[REPORTER_INITIALIZED] = reporterType
}
@@ -176,8 +177,8 @@ export async function main (inputArgv: string[]): Promise<void> {
// script with the same name, run the script instead of the built-in command.
const typedCommandName = argv.remain[0]
if (cmd != null && !builtInCommandForced && overridableByScriptCommands.has(typedCommandName) && !cliOptions.global) {
const currentDirManifest = config.dir === config.rootProjectManifestDir
? config.rootProjectManifest
const currentDirManifest = config.dir === context.rootProjectManifestDir
? context.rootProjectManifest
: await safeReadProjectManifestOnly(config.dir)
if (currentDirManifest?.scripts?.[typedCommandName]) {
// Redirect to "pnpm run <cmd>"
@@ -191,8 +192,8 @@ export async function main (inputArgv: string[]): Promise<void> {
}
} else if (
workspaceDir &&
config.dir !== config.rootProjectManifestDir &&
config.rootProjectManifest?.scripts?.[typedCommandName]
config.dir !== context.rootProjectManifestDir &&
context.rootProjectManifest?.scripts?.[typedCommandName]
) {
throw new PnpmError(
'SCRIPT_OVERRIDE_IN_WORKSPACE_ROOT',
@@ -261,9 +262,9 @@ export async function main (inputArgv: string[]): Promise<void> {
process.exitCode = config.failIfNoMatch ? 1 : 0
return
}
config.allProjectsGraph = filterResults.allProjectsGraph
config.selectedProjectsGraph = filterResults.selectedProjectsGraph
if (isEmpty(config.selectedProjectsGraph)) {
context.allProjectsGraph = filterResults.allProjectsGraph
context.selectedProjectsGraph = filterResults.selectedProjectsGraph
if (isEmpty(context.selectedProjectsGraph)) {
if (printLogs) {
console.log(`No projects matched the filters in "${wsDir}"`)
}
@@ -279,7 +280,7 @@ export async function main (inputArgv: string[]): Promise<void> {
if (filterResults.unmatchedFilters.length !== 0 && printLogs) {
console.log(`No projects matched the filters "${filterResults.unmatchedFilters.join(', ')}" in "${wsDir}"`)
}
config.allProjects = filterResults.allProjects
context.allProjects = filterResults.allProjects
config.workspaceDir = wsDir
}
@@ -313,16 +314,18 @@ export async function main (inputArgv: string[]): Promise<void> {
!cliOptions['recursive']
? { selected: 1 }
: {
selected: Object.keys(config.selectedProjectsGraph!).length,
total: config.allProjects!.length,
selected: Object.keys(context.selectedProjectsGraph!).length,
total: context.allProjects!.length,
}
),
...(workspaceDir ? { workspacePrefix: workspaceDir } : {}),
})
let result = pnpmCmds[cmd ?? 'help'](
// TypeScript doesn't currently infer that the type of config
// is `Omit<typeof config, 'reporter'>` after the `delete config.reporter` statement
config as Omit<typeof config, 'reporter'>,
// Spread config (settings) and context (runtime state) into a single
// options object for command handlers. The original split objects are
// also passed for handlers that need them separated (e.g. config commands).
// Named "_config"/"_context" to avoid clashing with the "--config" CLI option.
{ ...config, ...context, _config: config, _context: context } as Omit<typeof config & ConfigContext, 'reporter'>,
cliParams,
pnpmCmds
)

View File

@@ -1,5 +1,5 @@
import { initDefaultReporter } from '@pnpm/cli.default-reporter'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import type { Log } from '@pnpm/core-loggers'
import { type LogLevel, type StreamParser, streamParser, writeToConsole } from '@pnpm/logger'
@@ -11,7 +11,7 @@ export function initReporter (
reporterType: ReporterType,
opts: {
cmd: string | null
config: Config
config: Config & ConfigContext
}
): void {
switch (reporterType) {

View File

@@ -1,7 +1,7 @@
import path from 'node:path'
import { packageManager } from '@pnpm/cli.meta'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { installPnpmToStore } from '@pnpm/engine.pm.commands'
import { PnpmError } from '@pnpm/error'
import { isPackageManagerResolved, resolvePackageManagerIntegrities } from '@pnpm/installing.env-installer'
@@ -12,22 +12,22 @@ import { createStoreController } from '@pnpm/store.connection-manager'
import spawn from 'cross-spawn'
import semver from 'semver'
export async function switchCliVersion (config: Config): Promise<void> {
const pm = config.wantedPackageManager
export async function switchCliVersion (config: Config, context: ConfigContext): Promise<void> {
const pm = context.wantedPackageManager
if (pm == null || pm.name !== 'pnpm' || pm.version == null) return
let envLockfile = await readEnvLockfile(config.rootProjectManifestDir) ?? undefined
let envLockfile = await readEnvLockfile(context.rootProjectManifestDir) ?? undefined
let storeToUse: Awaited<ReturnType<typeof createStoreController>> | undefined
// Check if the env lockfile already has a resolved version that satisfies the wanted version/range.
let pmVersion = envLockfile?.importers['.'].packageManagerDependencies?.['pnpm']?.version
if (!pmVersion || !semver.satisfies(pmVersion, pm.version, { includePrerelease: true })) {
// Resolve to an exact version from the registry.
storeToUse = await createStoreController(config)
storeToUse = await createStoreController({ ...config, ...context })
envLockfile = await resolvePackageManagerIntegrities(pm.version, {
envLockfile,
registries: config.registries,
rootDir: config.rootProjectManifestDir,
rootDir: context.rootProjectManifestDir,
storeController: storeToUse.ctrl,
storeDir: storeToUse.dir,
})
@@ -38,11 +38,11 @@ export async function switchCliVersion (config: Config): Promise<void> {
return
}
} else if (!isPackageManagerResolved(envLockfile, pmVersion)) {
storeToUse = await createStoreController(config)
storeToUse = await createStoreController({ ...config, ...context })
envLockfile = await resolvePackageManagerIntegrities(pmVersion, {
envLockfile,
registries: config.registries,
rootDir: config.rootProjectManifestDir,
rootDir: context.rootProjectManifestDir,
storeController: storeToUse.ctrl,
storeDir: storeToUse.dir,
})
@@ -57,7 +57,7 @@ export async function switchCliVersion (config: Config): Promise<void> {
// We need a store controller to install pnpm. If it wasn't created during
// integrity resolution (because integrities were already cached), create it now.
if (!storeToUse) {
storeToUse = await createStoreController(config)
storeToUse = await createStoreController({ ...config, ...context })
}
if (!envLockfile) {

View File

@@ -1,10 +1,10 @@
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import type {
LogBase,
ReadPackageHook,
} from '@pnpm/types'
export type PnpmOptions = Omit<Config, 'reporter' | 'pnpmfile'> & {
export type PnpmOptions = Omit<Config & ConfigContext, 'reporter' | 'pnpmfile'> & {
argv: {
cooked: string[]
original: string[]

View File

@@ -64,7 +64,7 @@ describe('calcPnpmfilePathsOfPluginDeps', () => {
test('hoist: false removes hoistPattern', async () => {
prepare()
const config = await getConfig({
const { config } = await getConfig({
hoist: false,
}, {
workspaceDir: '.',

View File

@@ -6,7 +6,7 @@ import { getBinsFromPackageManifest } from '@pnpm/bins.resolver'
import type { Catalogs } from '@pnpm/catalogs.types'
import { FILTERING } from '@pnpm/cli.common-cli-options-help'
import { readProjectManifest } from '@pnpm/cli.utils'
import { type Config, getDefaultWorkspaceConcurrency, getWorkspaceConcurrency, types as allTypes, type UniversalOptions } from '@pnpm/config.reader'
import { type Config, type ConfigContext, getDefaultWorkspaceConcurrency, getWorkspaceConcurrency, types as allTypes, type UniversalOptions } from '@pnpm/config.reader'
import { PnpmError } from '@pnpm/error'
import { packlist } from '@pnpm/fs.packlist'
import type { Hooks } from '@pnpm/hooks.pnpmfile'
@@ -102,11 +102,12 @@ export type PackOptions = Pick<UniversalOptions, 'dir'> & Pick<Config, 'catalogs
| 'userAgent'
> & Partial<Pick<Config, 'extraBinPaths'
| 'extraEnv'
| 'hooks'
| 'recursive'
| 'selectedProjectsGraph'
| 'workspaceConcurrency'
| 'workspaceDir'
>> & Partial<Pick<ConfigContext,
| 'hooks'
| 'selectedProjectsGraph'
>> & {
argv: {
original: string[]

View File

@@ -2,7 +2,7 @@ import path from 'node:path'
import { FILTERING } from '@pnpm/cli.common-cli-options-help'
import { docsUrl, readProjectManifest } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { PnpmError } from '@pnpm/error'
import { runLifecycleHook, type RunLifecycleHookOptions } from '@pnpm/exec.lifecycle'
import { getCurrentBranch, isGitRepo, isRemoteHistoryClean, isWorkingTreeClean } from '@pnpm/network.git-utils'
@@ -122,7 +122,8 @@ export async function handler (
engineStrict?: boolean
recursive?: boolean
workspaceDir?: string
} & Pick<Config, 'allProjects' | 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme'>,
} & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme'>
& Pick<ConfigContext, 'allProjects'>,
params: string[]
): Promise<{ exitCode?: number } | undefined> {
const result = await publish(opts, params)
@@ -143,7 +144,8 @@ export async function publish (
engineStrict?: boolean
recursive?: boolean
workspaceDir?: string
} & Pick<Config, 'allProjects' | 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'packGzipLevel'>,
} & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'packGzipLevel'>
& Pick<ConfigContext, 'allProjects'>,
params: string[]
): Promise<PublishResult> {
if (opts.gitChecks !== false && await isGitRepo()) {

View File

@@ -1,7 +1,7 @@
import path from 'node:path'
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { createResolver } from '@pnpm/installing.client'
import { logger } from '@pnpm/logger'
import type { ResolveFunction } from '@pnpm/resolving.resolver-base'
@@ -17,13 +17,15 @@ import type { PublishPackedPkgOptions } from './publishPackedPkg.js'
export type PublishRecursiveOpts = Required<Pick<Config,
| 'bin'
| 'cacheDir'
| 'cliOptions'
| 'dir'
| 'pnpmHomeDir'
| 'authConfig'
| 'registries'
| 'workspaceDir'
>> &
Required<Pick<ConfigContext,
| 'cliOptions'
>> &
Partial<Pick<Config,
| 'tag'
| 'ca'
@@ -46,12 +48,14 @@ Partial<Pick<Config,
| 'noProxy'
| 'npmPath'
| 'offline'
| 'selectedProjectsGraph'
| 'strictSsl'
| 'unsafePerm'
| 'userAgent'
| 'userConfig'
| 'verifyStoreIntegrity'
>> &
Partial<Pick<ConfigContext,
| 'selectedProjectsGraph'
>> & {
access?: 'public' | 'restricted'
argv: {
@@ -61,7 +65,7 @@ Partial<Pick<Config,
} & PublishPackedPkgOptions
export async function recursivePublish (
opts: PublishRecursiveOpts & Required<Pick<Config, 'selectedProjectsGraph'>>
opts: PublishRecursiveOpts & Required<Pick<ConfigContext, 'selectedProjectsGraph'>>
): Promise<{ exitCode: number }> {
const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package)
const { resolve } = createResolver({

View File

@@ -1,7 +1,7 @@
import { promises as fs } from 'node:fs'
import { packageManager } from '@pnpm/cli.meta'
import type { Config } from '@pnpm/config.reader'
import type { Config, ConfigContext } from '@pnpm/config.reader'
import { type ClientOptions, createClient } from '@pnpm/installing.client'
import { type CafsLocker, createPackageStore, type StoreController } from '@pnpm/store.controller'
import { StoreIndex } from '@pnpm/store.index'
@@ -28,7 +28,6 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
| 'fetchMinSpeedKiBps'
| 'gitShallowHosts'
| 'ignoreScripts'
| 'hooks'
| 'httpProxy'
| 'httpsProxy'
| 'key'
@@ -51,7 +50,7 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
| 'userAgent'
| 'verifyStoreIntegrity'
| 'virtualStoreDirMaxLength'
> & {
> & Pick<ConfigContext, 'hooks'> & {
cafsLocker?: CafsLocker
ignoreFile?: (filename: string) => boolean
fetchFullMetadata?: boolean

View File

@@ -3,7 +3,7 @@ import path from 'node:path'
import { packageManager } from '@pnpm/cli.meta'
import { docsUrl } from '@pnpm/cli.utils'
import { type Config, types as allTypes } from '@pnpm/config.reader'
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
import { PnpmError } from '@pnpm/error'
import { sortKeysByPriority } from '@pnpm/object.key-sorting'
import type { ProjectManifest } from '@pnpm/types'
@@ -55,7 +55,7 @@ export function help (): string {
}
export type InitOptions =
& Pick<Config, 'cliOptions'>
& Pick<ConfigContext, 'cliOptions'>
& Partial<Pick<Config,
| 'initPackageManager'
| 'initType'