From 3033bee4302d9a4de548d2b5c17f2bd8f2d40679 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Sat, 4 Apr 2026 23:44:25 +0200 Subject: [PATCH] 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 & Pick 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. --- .../after-install/src/extendBuildOptions.ts | 4 +- building/commands/src/build/rebuild.ts | 15 +- building/commands/src/build/recursive.ts | 10 +- building/commands/src/policy/approveBuilds.ts | 4 +- .../test/policy/approveBuilds.test.ts | 10 +- cache/commands/src/cache.cmd.ts | 4 +- cli/default-reporter/src/index.ts | 6 +- .../src/reporterForClient/index.ts | 4 +- .../reporterForClient/reportIgnoredBuilds.ts | 4 +- cli/default-reporter/test/index.ts | 46 +++---- .../test/reportingDeprecations.ts | 4 +- .../test/reportingProgress.ts | 22 +-- cli/default-reporter/test/reportingScope.ts | 10 +- .../test/reportingUpdateCheck.ts | 8 +- cli/parse-cli-args/src/index.ts | 25 +++- config/commands/src/ConfigCommandOptions.ts | 10 +- config/commands/src/configGet.ts | 12 +- config/commands/src/configList.ts | 4 +- config/commands/src/configToRecord.ts | 21 ++- config/commands/test/configDelete.test.ts | 26 ++-- config/commands/test/configGet.test.ts | 55 ++++---- config/commands/test/configList.test.ts | 18 +-- config/commands/test/configSet.test.ts | 130 +++++++++--------- .../test/managingAuthSettings.test.ts | 37 ++--- config/commands/test/utils/index.ts | 33 +++++ config/reader/src/Config.ts | 46 +++++-- config/reader/src/auth.test.ts | 64 +++++---- config/reader/src/auth.ts | 6 +- config/reader/src/index.ts | 33 ++++- config/reader/src/inheritPickedConfig.ts | 17 ++- config/reader/test/index.ts | 12 +- deps/compliance/commands/src/audit/audit.ts | 7 +- .../commands/src/licenses/licensesList.ts | 7 +- deps/compliance/commands/src/sbom/sbom.ts | 9 +- deps/inspection/commands/src/listing/list.ts | 11 +- .../commands/src/outdated/outdated.ts | 7 +- deps/inspection/commands/src/peers.ts | 6 +- deps/inspection/commands/src/view/index.ts | 4 +- deps/inspection/commands/test/view.ts | 42 +++--- deps/status/src/checkDepsStatus.ts | 11 +- .../commands/src/self-updater/selfUpdate.ts | 3 +- engine/runtime/commands/src/env/node.ts | 7 +- exec/commands/src/exec.ts | 7 +- exec/commands/src/run.ts | 8 +- exec/commands/src/runRecursive.ts | 5 +- installing/commands/src/fetch.ts | 4 +- installing/commands/src/import/index.ts | 9 +- installing/commands/src/install.ts | 19 +-- installing/commands/src/installDeps.ts | 19 +-- installing/commands/src/link.ts | 9 +- installing/commands/src/recursive.ts | 10 +- installing/commands/src/remove.ts | 17 +-- patching/commands/src/patch.ts | 5 +- patching/commands/src/patchCommit.ts | 4 +- patching/commands/src/patchRemove.ts | 4 +- pnpm/src/getConfig.ts | 32 +++-- pnpm/src/main.ts | 45 +++--- pnpm/src/reporter/index.ts | 4 +- pnpm/src/switchCliVersion.ts | 18 +-- pnpm/src/types.ts | 4 +- pnpm/test/getConfig.test.ts | 2 +- releasing/commands/src/publish/pack.ts | 7 +- releasing/commands/src/publish/publish.ts | 8 +- .../commands/src/publish/recursivePublish.ts | 12 +- .../src/createNewStoreController.ts | 5 +- workspace/commands/src/init.ts | 4 +- 66 files changed, 602 insertions(+), 473 deletions(-) diff --git a/building/after-install/src/extendBuildOptions.ts b/building/after-install/src/extendBuildOptions.ts index 2f693a2337..7798380c9e 100644 --- a/building/after-install/src/extendBuildOptions.ts +++ b/building/after-install/src/extendBuildOptions.ts @@ -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 export type BuildOptions = Partial & -Pick & Pick +Pick & Pick const defaults = async (opts: BuildOptions): Promise => { const packageManager = opts.packageManager ?? diff --git a/building/commands/src/build/rebuild.ts b/building/commands/src/build/rebuild.ts index a18c25fe0f..bee869cfe8 100644 --- a/building/commands/src/build/rebuild.ts +++ b/building/commands/src/build/rebuild.ts @@ -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 & Pick & CreateStoreControllerOptions & { diff --git a/building/commands/src/build/recursive.ts b/building/commands/src/build/recursive.ts index 60ee363487..d04ef3690a 100755 --- a/building/commands/src/build/recursive.ts +++ b/building/commands/src/build/recursive.ts @@ -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 & Pick & { pending?: boolean } & Partial> @@ -40,7 +42,7 @@ export async function recursiveRebuild ( params: string[], opts: RecursiveRebuildOpts & { ignoredPackages?: Set - } & Required> + } & Required> & Required> ): Promise { if (allProjects.length === 0) { // It might make sense to throw an exception in this case diff --git a/building/commands/src/policy/approveBuilds.ts b/building/commands/src/policy/approveBuilds.ts index b2e81e10d4..bc44c9b107 100644 --- a/building/commands/src/policy/approveBuilds.ts +++ b/building/commands/src/policy/approveBuilds.ts @@ -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 & { all?: boolean, global?: boolean } +export type ApproveBuildsCommandOpts = Pick & Pick & { all?: boolean, global?: boolean } export const commandNames = ['approve-builds'] diff --git a/building/commands/test/policy/approveBuilds.test.ts b/building/commands/test/policy/approveBuilds.test.ts index 3fe4d49482..f7a01b106b 100644 --- a/building/commands/test/policy/approveBuilds.test.ts +++ b/building/commands/test/policy/approveBuilds.test.ts @@ -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[] diff --git a/cache/commands/src/cache.cmd.ts b/cache/commands/src/cache.cmd.ts index 6ffeb85e66..1c3352e7f6 100644 --- a/cache/commands/src/cache.cmd.ts +++ b/cache/commands/src/cache.cmd.ts @@ -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 +export type CacheCommandOptions = Pick & Pick export async function handler (opts: CacheCommandOptions, params: string[]): Promise { const cacheType = (opts.resolutionMode === 'time-based' && !opts.registrySupportsTimeField) diff --git a/cli/default-reporter/src/index.ts b/cli/default-reporter/src/index.ts index b5c437e933..65d13ba3a5 100644 --- a/cli/default-reporter/src/index.ts +++ b/cli/default-reporter/src/index.ts @@ -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 } diff --git a/cli/default-reporter/src/reporterForClient/index.ts b/cli/default-reporter/src/reporterForClient/index.ts index 88d6d02f94..ae4c79b2f5 100644 --- a/cli/default-reporter/src/reporterForClient/index.ts +++ b/cli/default-reporter/src/reporterForClient/index.ts @@ -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 diff --git a/cli/default-reporter/src/reporterForClient/reportIgnoredBuilds.ts b/cli/default-reporter/src/reporterForClient/reportIgnoredBuilds.ts index 7afc952c25..13f1f91a76 100644 --- a/cli/default-reporter/src/reporterForClient/reportIgnoredBuilds.ts +++ b/cli/default-reporter/src/reporterForClient/reportIgnoredBuilds.ts @@ -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 }, opts: { - pnpmConfig?: Config + pnpmConfig?: Config & ConfigContext // This is used by Bit CLI approveBuildsInstructionText?: string } diff --git a/cli/default-reporter/test/index.ts b/cli/default-reporter/test/index.ts index a569dfb770..f496a997cd 100644 --- a/cli/default-reporter/test/index.ts +++ b/cli/default-reporter/test/index.ts @@ -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, diff --git a/cli/default-reporter/test/reportingDeprecations.ts b/cli/default-reporter/test/reportingDeprecations.ts index 65d962653c..8fe0cf2087 100644 --- a/cli/default-reporter/test/reportingDeprecations.ts +++ b/cli/default-reporter/test/reportingDeprecations.ts @@ -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(), }) diff --git a/cli/default-reporter/test/reportingProgress.ts b/cli/default-reporter/test/reportingProgress.ts index 77f7695349..db625ca8b2 100644 --- a/cli/default-reporter/test/reportingProgress.ts +++ b/cli/default-reporter/test/reportingProgress.ts @@ -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(), diff --git a/cli/default-reporter/test/reportingScope.ts b/cli/default-reporter/test/reportingScope.ts index 55b4c66329..283a377d9d 100644 --- a/cli/default-reporter/test/reportingScope.ts +++ b/cli/default-reporter/test/reportingScope.ts @@ -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(), }) diff --git a/cli/default-reporter/test/reportingUpdateCheck.ts b/cli/default-reporter/test/reportingUpdateCheck.ts index 632e7607a1..848f797d97 100644 --- a/cli/default-reporter/test/reportingUpdateCheck.ts +++ b/cli/default-reporter/test/reportingUpdateCheck.ts @@ -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', }, diff --git a/cli/parse-cli-args/src/index.ts b/cli/parse-cli-args/src/index.ts index e7744925a1..a126dd12fa 100644 --- a/cli/parse-cli-args/src/index.ts +++ b/cli/parse-cli-args/src/index.ts @@ -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 diff --git a/config/commands/src/ConfigCommandOptions.ts b/config/commands/src/ConfigCommandOptions.ts index 3b69bafc29..593ff7c8f6 100644 --- a/config/commands/src/ConfigCommandOptions.ts +++ b/config/commands/src/ConfigCommandOptions.ts @@ -1,16 +1,16 @@ -import type { Config } from '@pnpm/config.reader' +import type { Config, ConfigContext } from '@pnpm/config.reader' export type ConfigCommandOptions = Pick & Pick & { + _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 } diff --git a/config/commands/src/configGet.ts b/config/commands/src/configGet.ts index f9bc929270..60b2d862ee 100644 --- a/config/commands/src/configGet.ts +++ b/config/commands/src/configGet.ts @@ -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)[camelKey] } + return { value: (opts._config as unknown as Record)[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 { 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), } diff --git a/config/commands/src/configList.ts b/config/commands/src/configList.ts index d2989253a6..fb3b4c8b49 100644 --- a/config/commands/src/configList.ts +++ b/config/commands/src/configList.ts @@ -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 { - return JSON.stringify(configToRecord(opts as unknown as Config), undefined, 2) + return JSON.stringify(configToRecord(opts._config, opts._context.explicitlySetKeys), undefined, 2) } diff --git a/config/commands/src/configToRecord.ts b/config/commands/src/configToRecord.ts index 35b8819bdb..3fdd29d064 100644 --- a/config/commands/src/configToRecord.ts +++ b/config/commands/src/configToRecord.ts @@ -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 { +export function configToRecord (config: Config, explicitlySetKeys: Set): Record { const result: Record = {} - 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)[camelKey] if (value !== undefined) { result[camelKey] = value @@ -31,8 +30,8 @@ export function configToRecord (config: Config): Record { } // 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 } } diff --git a/config/commands/test/configDelete.test.ts b/config/commands/test/configDelete.test.ts index d526fe4c24..1afc6acdaa 100644 --- a/config/commands/test/configDelete.test.ts +++ b/config/commands/test/configDelete.test.ts @@ -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') }) diff --git a/config/commands/test/configGet.test.ts b/config/commands/test/configGet.test.ts index 76fa91f364..72c1c015f4 100644 --- a/config/commands/test/configGet.test.ts +++ b/config/commands/test/configGet.test.ts @@ -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') }) diff --git a/config/commands/test/configList.test.ts b/config/commands/test/configList.test.ts index dc4b86a2fb..3653603f6e 100644 --- a/config/commands/test/configList.test.ts +++ b/config/commands/test/configList.test.ts @@ -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', diff --git a/config/commands/test/configSet.test.ts b/config/commands/test/configSet.test.ts index 4889bff026..9d76d0d6da 100644 --- a/config/commands/test/configSet.test.ts +++ b/config/commands/test/configSet.test.ts @@ -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, diff --git a/config/commands/test/managingAuthSettings.test.ts b/config/commands/test/managingAuthSettings.test.ts index b5088ca51a..2c7279c184 100644 --- a/config/commands/test/managingAuthSettings.test.ts +++ b/config/commands/test/managingAuthSettings.test.ts @@ -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, diff --git a/config/commands/test/utils/index.ts b/config/commands/test/utils/index.ts index de7d770688..b4ec7585a7 100644 --- a/config/commands/test/utils/index.ts +++ b/config/commands/test/utils/index.ts @@ -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 & { + dir: string + configDir: string + cliOptions: Record + authConfig: Record + 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 diff --git a/config/reader/src/Config.ts b/config/reader/src/Config.ts index e1f9a09704..426534ed77 100644 --- a/config/reader/src/Config.ts +++ b/config/reader/src/Config.ts @@ -14,22 +14,50 @@ import type { import type { OptionsFromRootManifest } from './getOptionsFromRootManifest.js' import type { AuthInfo } from './parseAuthInfo.js' -export type UniversalOptions = Pick +export type UniversalOptions = Pick & Pick 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 + + // -- Workspace context -- allProjects?: Project[] selectedProjectsGraph?: ProjectsGraph allProjectsGraph?: ProjectsGraph + rootProjectManifest?: ProjectManifest + rootProjectManifestDir: string + // -- CLI metadata -- + cliOptions: Record // eslint-disable-line + rawLocalConfig: Record // eslint-disable-line + /** Keys explicitly set from workspace yaml, CLI, or env vars (not defaults). */ + explicitlySetKeys: Set + 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, // 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, // eslint-disable-line authConfig: Record, // eslint-disable-line - /** Keys explicitly set from workspace yaml, CLI, or env vars (not defaults). */ - explicitlySetKeys: Set 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 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 hoist: boolean diff --git a/config/reader/src/auth.test.ts b/config/reader/src/auth.test.ts index e4297b07a8..a4976da763 100644 --- a/config/reader/src/auth.test.ts +++ b/config/reader/src/auth.test.ts @@ -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', }) }) diff --git a/config/reader/src/auth.ts b/config/reader/src/auth.ts index 4ce50e7588..b6fde03b5f 100644 --- a/config/reader/src/auth.ts +++ b/config/reader/src/auth.ts @@ -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): Partial { return result as Partial } -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) } /** diff --git a/config/reader/src/index.ts b/config/reader/src/index.ts index 19d9478592..78f0f4bfb7 100644 --- a/config/reader/src/index.ts +++ b/config/reader/src/index.ts @@ -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 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(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, diff --git a/config/reader/src/inheritPickedConfig.ts b/config/reader/src/inheritPickedConfig.ts index a37a89f416..f65a610623 100644 --- a/config/reader/src/inheritPickedConfig.ts +++ b/config/reader/src/inheritPickedConfig.ts @@ -1,17 +1,20 @@ -import type { Config } from './Config.js' +import type { Config, ConfigContext } from './Config.js' -export type InheritableConfig = Partial & Pick +export interface InheritableConfigPair { + config: Partial & Pick + context: Pick +} export type PickConfig = (cfg: Partial) => Partial export type PickRawConfig = (cfg: Record) => Record 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)) } diff --git a/config/reader/test/index.ts b/config/reader/test/index.ts index c4b93ee6e1..695115b31d 100644 --- a/config/reader/test/index.ts +++ b/config/reader/test/index.ts @@ -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, }) diff --git a/deps/compliance/commands/src/audit/audit.ts b/deps/compliance/commands/src/audit/audit.ts index 41af89fcbc..5f71b3777c 100644 --- a/deps/compliance/commands/src/audit/audit.ts +++ b/deps/compliance/commands/src/audit/audit.ts @@ -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 & { | 'optional' | 'userConfig' | 'authConfig' -| 'rootProjectManifest' -| 'rootProjectManifestDir' | 'virtualStoreDirMaxLength' | 'workspaceDir' +> & Pick & InstallCommandOptions const DEFAULT_FIX_METHOD = 'override' diff --git a/deps/compliance/commands/src/licenses/licensesList.ts b/deps/compliance/commands/src/licenses/licensesList.ts index 85e220aa72..dbe51fddf1 100644 --- a/deps/compliance/commands/src/licenses/licensesList.ts +++ b/deps/compliance/commands/src/licenses/licensesList.ts @@ -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 & Partial> diff --git a/deps/compliance/commands/src/sbom/sbom.ts b/deps/compliance/commands/src/sbom/sbom.ts index 46525a43c7..6d52df01b3 100644 --- a/deps/compliance/commands/src/sbom/sbom.ts +++ b/deps/compliance/commands/src/sbom/sbom.ts @@ -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 & Partial> diff --git a/deps/inspection/commands/src/listing/list.ts b/deps/inspection/commands/src/listing/list.ts index 9af3132dae..9fc6719fa4 100644 --- a/deps/inspection/commands/src/listing/list.ts +++ b/deps/inspection/commands/src/listing/list.ts @@ -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 & Partial> & { +> & Pick & Partial> & { alwaysPrintRootPackage?: boolean depth?: number excludePeers?: boolean diff --git a/deps/inspection/commands/src/outdated/outdated.ts b/deps/inspection/commands/src/outdated/outdated.ts index 19a948ec41..7b60f29b8c 100644 --- a/deps/inspection/commands/src/outdated/outdated.ts +++ b/deps/inspection/commands/src/outdated/outdated.ts @@ -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 & Pick & Partial> export async function handler ( diff --git a/deps/inspection/commands/src/peers.ts b/deps/inspection/commands/src/peers.ts index 2ae2a4e695..034d21ea30 100644 --- a/deps/inspection/commands/src/peers.ts +++ b/deps/inspection/commands/src/peers.ts @@ -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 & Partial> & { +> & Pick +& Partial> & { json?: boolean lockfileDir?: string lockfileOnly?: boolean diff --git a/deps/inspection/commands/src/view/index.ts b/deps/inspection/commands/src/view/index.ts index 0b5f7662b6..5929598463 100644 --- a/deps/inspection/commands/src/view/index.ts +++ b/deps/inspection/commands/src/view/index.ts @@ -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[] diff --git a/deps/inspection/commands/test/view.ts b/deps/inspection/commands/test/view.ts index 8b6d246c28..6d4b7d405c 100644 --- a/deps/inspection/commands/test/view.ts +++ b/deps/inspection/commands/test/view.ts @@ -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() diff --git a/deps/status/src/checkDepsStatus.ts b/deps/status/src/checkDepsStatus.ts index 5ec6a55fcd..07fc265ca0 100644 --- a/deps/status/src/checkDepsStatus.ts +++ b/deps/status/src/checkDepsStatus.ts @@ -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 & Pick & { ignoreFilteredInstallCache?: boolean ignoredWorkspaceStateSettings?: Array diff --git a/engine/pm/commands/src/self-updater/selfUpdate.ts b/engine/pm/commands/src/self-updater/selfUpdate.ts index e1b28d6547..e94c1bbdf0 100644 --- a/engine/pm/commands/src/self-updater/selfUpdate.ts +++ b/engine/pm/commands/src/self-updater/selfUpdate.ts @@ -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 & Pick diff --git a/engine/runtime/commands/src/env/node.ts b/engine/runtime/commands/src/env/node.ts index e2b81e05f3..bf8d6b7c65 100644 --- a/engine/runtime/commands/src/env/node.ts +++ b/engine/runtime/commands/src/env/node.ts @@ -1,4 +1,4 @@ -import type { Config } from '@pnpm/config.reader' +import type { Config, ConfigContext } from '@pnpm/config.reader' export type NvmNodeCommandOptions = Pick & Partial> & Partial> & { remote?: boolean } diff --git a/exec/commands/src/exec.ts b/exec/commands/src/exec.ts index 50dba1df27..330f0ad89e 100644 --- a/exec/commands/src/exec.ts +++ b/exec/commands/src/exec.ts @@ -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> & { +export type ExecOpts = Required> & { bail?: boolean unsafePerm?: boolean reverse?: boolean @@ -148,7 +148,6 @@ export type ExecOpts = Required> & { implicitlyFellbackFromRun?: boolean } & Pick> & { | 'userAgent' | 'verifyDepsBeforeRun' | 'workspaceDir' -> & CheckDepsStatusOptions +> & Pick & CheckDepsStatusOptions export async function handler ( opts: ExecOpts, diff --git a/exec/commands/src/run.ts b/exec/commands/src/run.ts index 33925f19c1..5199b3b6ed 100644 --- a/exec/commands/src/run.ts +++ b/exec/commands/src/run.ts @@ -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 + & Pick & ( - | { recursive?: false } & Partial> - | { recursive: true } & Required> + | { recursive?: false } & Partial & Pick> + | { recursive: true } & Required & Pick> ) & { argv?: { diff --git a/exec/commands/src/runRecursive.ts b/exec/commands/src/runRecursive.ts index 9572fef3e8..6623102a81 100644 --- a/exec/commands/src/runRecursive.ts +++ b/exec/commands/src/runRecursive.ts @@ -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 & Required> & +> & Pick & Required & Pick> & Partial> & { ifPresent?: boolean diff --git a/installing/commands/src/fetch.ts b/installing/commands/src/fetch.ts index 3c6293165a..6f71478e03 100644 --- a/installing/commands/src/fetch.ts +++ b/installing/commands/src/fetch.ts @@ -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 & CreateStoreControllerOptions +type FetchCommandOptions = Pick & Pick & CreateStoreControllerOptions export async function handler (opts: FetchCommandOptions): Promise { const store = await createStoreController(opts) diff --git a/installing/commands/src/import/index.ts b/installing/commands/src/import/index.ts index 61f9a9bfa2..fe72240020 100644 --- a/installing/commands/src/import/index.ts +++ b/installing/commands/src/import/index.ts @@ -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 & Pick & CreateStoreControllerOptions & Omit diff --git a/installing/commands/src/install.ts b/installing/commands/src/install.ts index c463151a41..5e0ef1c9da 100644 --- a/installing/commands/src/install.ts +++ b/installing/commands/src/install.ts @@ -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 & Pick & CreateStoreControllerOptions & Partial> & { argv: { original: string[] diff --git a/installing/commands/src/installDeps.ts b/installing/commands/src/installDeps.ts index e7b62df235..c776cfbbad 100644 --- a/installing/commands/src/installDeps.ts +++ b/installing/commands/src/installDeps.ts @@ -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 & Pick & CreateStoreControllerOptions & { argv: { original: string[] diff --git a/installing/commands/src/link.ts b/installing/commands/src/link.ts index 470904c689..e26c487523 100644 --- a/installing/commands/src/link.ts +++ b/installing/commands/src/link.ts @@ -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 & Pick & Partial> & install.InstallCommandOptions export const rcOptionsTypes = cliOptionsTypes diff --git a/installing/commands/src/recursive.ts b/installing/commands/src/recursive.ts index a48fb45da6..a5e7067047 100755 --- a/installing/commands/src/recursive.ts +++ b/installing/commands/src/recursive.ts @@ -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 & Pick & { rebuildHandler?: CommandHandler include?: IncludedDependencies diff --git a/installing/commands/src/remove.ts b/installing/commands/src/remove.ts index e4488507d6..b016e1e421 100644 --- a/installing/commands/src/remove.ts +++ b/installing/commands/src/remove.ts @@ -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 & Pick & { recursive?: boolean pnpmfile: string[] diff --git a/patching/commands/src/patch.ts b/patching/commands/src/patch.ts index 4644f0f10b..a9cb01b928 100644 --- a/patching/commands/src/patch.ts +++ b/patching/commands/src/patch.ts @@ -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 & Pick & CreateStoreControllerOptions & { editDir?: string reporter?: (logObj: LogBase) => void diff --git a/patching/commands/src/patchCommit.ts b/patching/commands/src/patchCommit.ts index dbb961343d..5ab04df89e 100644 --- a/patching/commands/src/patchCommit.ts +++ b/patching/commands/src/patchCommit.ts @@ -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 +type PatchCommitCommandOptions = install.InstallCommandOptions & Pick & Pick export async function handler (opts: PatchCommitCommandOptions, params: string[]): Promise { const userDir = params[0] diff --git a/patching/commands/src/patchRemove.ts b/patching/commands/src/patchRemove.ts index d20c9543c2..8b17f398bc 100644 --- a/patching/commands/src/patchRemove.ts +++ b/patching/commands/src/patchRemove.ts @@ -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 +export type PatchRemoveCommandOptions = install.InstallCommandOptions & Pick & Pick export async function handler (opts: PatchRemoveCommandOptions, params: string[]): Promise { let patchesToRemove = params diff --git a/pnpm/src/getConfig.ts b/pnpm/src/getConfig.ts index 21a7c94de5..fa1ff5120f 100644 --- a/pnpm/src/getConfig.ts +++ b/pnpm/src/getConfig.ts @@ -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 { - 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 { +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 { diff --git a/pnpm/src/main.ts b/pnpm/src/main.ts index 928374b8e1..ff07427aa4 100644 --- a/pnpm/src/main.ts +++ b/pnpm/src/main.ts @@ -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 { 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 { 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 { } } } - 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 { if (printLogs) { initReporter(reporterType, { cmd, - config, + config: { ...config, ...context }, }) global[REPORTER_INITIALIZED] = reporterType } @@ -176,8 +177,8 @@ export async function main (inputArgv: string[]): Promise { // 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 " @@ -191,8 +192,8 @@ export async function main (inputArgv: string[]): Promise { } } 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 { 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 { 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 { !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` after the `delete config.reporter` statement - config as Omit, + // 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, cliParams, pnpmCmds ) diff --git a/pnpm/src/reporter/index.ts b/pnpm/src/reporter/index.ts index d739ef9d23..cc71fe44af 100644 --- a/pnpm/src/reporter/index.ts +++ b/pnpm/src/reporter/index.ts @@ -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) { diff --git a/pnpm/src/switchCliVersion.ts b/pnpm/src/switchCliVersion.ts index 7998354840..1aa4732301 100644 --- a/pnpm/src/switchCliVersion.ts +++ b/pnpm/src/switchCliVersion.ts @@ -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 { - const pm = config.wantedPackageManager +export async function switchCliVersion (config: Config, context: ConfigContext): Promise { + 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> | 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 { 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 { // 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) { diff --git a/pnpm/src/types.ts b/pnpm/src/types.ts index b0483fdbe0..81500c0c49 100644 --- a/pnpm/src/types.ts +++ b/pnpm/src/types.ts @@ -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 & { +export type PnpmOptions = Omit & { argv: { cooked: string[] original: string[] diff --git a/pnpm/test/getConfig.test.ts b/pnpm/test/getConfig.test.ts index 04905e1276..e529f8c1a8 100644 --- a/pnpm/test/getConfig.test.ts +++ b/pnpm/test/getConfig.test.ts @@ -64,7 +64,7 @@ describe('calcPnpmfilePathsOfPluginDeps', () => { test('hoist: false removes hoistPattern', async () => { prepare() - const config = await getConfig({ + const { config } = await getConfig({ hoist: false, }, { workspaceDir: '.', diff --git a/releasing/commands/src/publish/pack.ts b/releasing/commands/src/publish/pack.ts index 89c52cfa46..cee4f0df4d 100644 --- a/releasing/commands/src/publish/pack.ts +++ b/releasing/commands/src/publish/pack.ts @@ -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 & Pick & Partial> & Partial> & { argv: { original: string[] diff --git a/releasing/commands/src/publish/publish.ts b/releasing/commands/src/publish/publish.ts index 47c44875e3..3f54457ec5 100644 --- a/releasing/commands/src/publish/publish.ts +++ b/releasing/commands/src/publish/publish.ts @@ -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, + } & Pick + & Pick, 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, + } & Pick + & Pick, params: string[] ): Promise { if (opts.gitChecks !== false && await isGitRepo()) { diff --git a/releasing/commands/src/publish/recursivePublish.ts b/releasing/commands/src/publish/recursivePublish.ts index 2682a8c18f..f5bb251ba8 100644 --- a/releasing/commands/src/publish/recursivePublish.ts +++ b/releasing/commands/src/publish/recursivePublish.ts @@ -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> & +Required> & Partial> & +Partial> & { access?: 'public' | 'restricted' argv: { @@ -61,7 +65,7 @@ Partial> + opts: PublishRecursiveOpts & Required> ): Promise<{ exitCode: number }> { const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package) const { resolve } = createResolver({ diff --git a/store/connection-manager/src/createNewStoreController.ts b/store/connection-manager/src/createNewStoreController.ts index b57e6ef81c..9f4fb2348f 100644 --- a/store/connection-manager/src/createNewStoreController.ts +++ b/store/connection-manager/src/createNewStoreController.ts @@ -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 & { +> & Pick & { cafsLocker?: CafsLocker ignoreFile?: (filename: string) => boolean fetchFullMetadata?: boolean diff --git a/workspace/commands/src/init.ts b/workspace/commands/src/init.ts index dc328d8528..976ed98959 100644 --- a/workspace/commands/src/init.ts +++ b/workspace/commands/src/init.ts @@ -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 + & Pick & Partial