From 97cf97609ecdebfc319e5d9cc77023527011341b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kh=E1=BA=A3i?= Date: Mon, 22 Dec 2025 18:26:14 +0700 Subject: [PATCH] fix(cli/config): phantom keys (#10323) * fix(cli/config): phantom keys Fixes https://github.com/pnpm/pnpm/issues/10296 This patch also include other refactors. * test: does not traverse the prototype chain * test: more properties * test: fix other tests * feat: revert unrelated changes --- .changeset/eighty-clowns-shave.md | 5 +++++ config/config/src/index.ts | 2 +- .../plugin-commands-config/src/configGet.ts | 2 +- .../plugin-commands-config/src/configSet.ts | 4 ++-- .../src/parseConfigPropertyPath.ts | 2 +- .../test/configGet.test.ts | 21 +++++++++++++++++++ 6 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 .changeset/eighty-clowns-shave.md diff --git a/.changeset/eighty-clowns-shave.md b/.changeset/eighty-clowns-shave.md new file mode 100644 index 0000000000..f93b3ce4c9 --- /dev/null +++ b/.changeset/eighty-clowns-shave.md @@ -0,0 +1,5 @@ +--- +"@pnpm/plugin-commands-config": patch +--- + +Fix phantom keys in `pnpm config get ` [#10296](https://github.com/pnpm/pnpm/issues/10296). diff --git a/config/config/src/index.ts b/config/config/src/index.ts index 10ddb872e8..16f0533b24 100644 --- a/config/config/src/index.ts +++ b/config/config/src/index.ts @@ -444,7 +444,7 @@ export async function getConfig (opts: { // TODO: should we throw some error or print some warning here? if (value === undefined) continue - if (key in cliOptions || kebabCase(key) in cliOptions) continue + if (Object.hasOwn(cliOptions, key) || Object.hasOwn(cliOptions, kebabCase(key))) continue // @ts-expect-error pnpmConfig[key] = value diff --git a/config/plugin-commands-config/src/configGet.ts b/config/plugin-commands-config/src/configGet.ts index 6f9bb9acb4..d91717deb5 100644 --- a/config/plugin-commands-config/src/configGet.ts +++ b/config/plugin-commands-config/src/configGet.ts @@ -35,7 +35,7 @@ function getRcConfig (rawConfig: Record, key: string, isScopedK return { value } } const rcKey = isCamelCase(key) ? kebabCase(key) : key - if (rcKey in types) { + if (Object.hasOwn(types, rcKey)) { const value = rawConfig[rcKey] return { value } } diff --git a/config/plugin-commands-config/src/configSet.ts b/config/plugin-commands-config/src/configSet.ts index 2baebb3713..adfd8b2f08 100644 --- a/config/plugin-commands-config/src/configSet.ts +++ b/config/plugin-commands-config/src/configSet.ts @@ -185,7 +185,7 @@ export class ConfigSetUnsupportedIniConfigKeyError extends PnpmError { */ function validateIniConfigKey (key: string): string { const kebabKey = kebabCase(key) - if (kebabKey in types) { + if (Object.hasOwn(types, kebabKey)) { return kebabKey } throw new ConfigSetUnsupportedIniConfigKeyError(key) @@ -207,7 +207,7 @@ export class ConfigSetUnsupportedWorkspaceKeyError extends PnpmError { * Return the camelCase of {@link key} if it's valid. */ function validateWorkspaceKey (key: string): string { - if (key in types) return camelCase(key) + if (Object.hasOwn(types, key)) return camelCase(key) if (!isCamelCase(key)) throw new ConfigSetUnsupportedWorkspaceKeyError(key) return key } diff --git a/config/plugin-commands-config/src/parseConfigPropertyPath.ts b/config/plugin-commands-config/src/parseConfigPropertyPath.ts index a0a9c777a0..19c7fd137e 100644 --- a/config/plugin-commands-config/src/parseConfigPropertyPath.ts +++ b/config/plugin-commands-config/src/parseConfigPropertyPath.ts @@ -24,7 +24,7 @@ function normalizeTopLevelConfigName (configName: string | number): string { if (typeof configName === 'number') return configName.toString() const kebabKey = kebabCase(configName) - if (kebabKey in types) return kebabKey + if (Object.hasOwn(types, kebabKey)) return kebabKey return configName } diff --git a/config/plugin-commands-config/test/configGet.test.ts b/config/plugin-commands-config/test/configGet.test.ts index 1921ee87d1..b9d0bd8ccd 100644 --- a/config/plugin-commands-config/test/configGet.test.ts +++ b/config/plugin-commands-config/test/configGet.test.ts @@ -302,3 +302,24 @@ test('config get npm-globalconfig', async () => { expect(getOutputString(getResult)).toBe(npmGlobalconfigPath) }) + +describe('does not traverse the prototype chain (#10296)', () => { + test.each([ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'toString', + 'valueOf', + '__proto__', + ])('%s', async key => { + const getResult = await config.handler({ + dir: process.cwd(), + cliOptions: {}, + configDir: process.cwd(), + global: true, + rawConfig: {}, + }, ['get', key]) + + expect(getOutputString(getResult)).toBe('undefined') + }) +})