feat(cli/config)!: config command outputs changed from INI to JSON with camelCase keys (#10142)

This commit is contained in:
Khải
2025-10-30 17:34:48 +07:00
committed by GitHub
parent 2d8c6307f5
commit d4bf2d0e60
9 changed files with 47 additions and 75 deletions

View File

@@ -0,0 +1,9 @@
---
"@pnpm/plugin-commands-config": major
"pnpm": major
---
`pnpm config get` (without `--json`) no longer print INI formatted text.
Instead, it would print JSON for both objects and arrays and raw string for
strings, numbers, booleans, and nulls.
`pnpm config get --json` would still print all types of values as JSON like before.

View File

@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-config": major
"pnpm": major
---
`pnpm config list` now prints a JSON object instead of INI formatted text.

View File

@@ -58,7 +58,7 @@ export function help (): string {
name: '--location <project|global>',
},
{
description: 'Show all the config settings in JSON format',
description: 'Show all types of values in JSON format (not just objects and arrays)',
name: '--json',
},
],
@@ -68,9 +68,9 @@ export function help (): string {
usages: [
'pnpm config set <key> <value>',
'pnpm config get <key>',
'pnpm config get --json <key>',
'pnpm config delete <key>',
'pnpm config list',
'pnpm config list --json',
],
})
}

View File

@@ -1,5 +1,4 @@
import kebabCase from 'lodash.kebabcase'
import { encode } from 'ini'
import { types } from '@pnpm/config'
import { isCamelCase, isStrictlyKebabCase } from '@pnpm/naming-cases'
import { getObjectValueByPropertyPath } from '@pnpm/object.property-path'
@@ -42,13 +41,11 @@ function getRcConfig (rawConfig: Record<string, unknown>, key: string, isScopedK
return undefined
}
type GetConfigByPropertyPathOptions = Pick<ConfigCommandOptions, 'json'>
function getConfigByPropertyPath (rawConfig: Record<string, unknown>, propertyPath: string, opts?: GetConfigByPropertyPathOptions): Found<unknown> {
function getConfigByPropertyPath (rawConfig: Record<string, unknown>, propertyPath: string): Found<unknown> {
const parsedPropertyPath = Array.from(parseConfigPropertyPath(propertyPath))
if (parsedPropertyPath.length === 0) {
return {
value: processConfig(rawConfig, opts),
value: processConfig(rawConfig),
}
}
return {
@@ -63,7 +60,7 @@ function displayConfig (config: unknown, opts: DisplayConfigOptions): string {
return JSON.stringify(config, undefined, 2)
}
if (typeof config === 'object' && config != null) {
return encode(config)
return JSON.stringify(config, undefined, 2)
}
return String(config)
}

View File

@@ -1,11 +1,9 @@
import { encode } from 'ini'
import { processConfig } from './processConfig.js'
import { type ConfigCommandOptions } from './ConfigCommandOptions.js'
export async function configList (opts: ConfigCommandOptions): Promise<string> {
const processedConfig = processConfig(opts.rawConfig, opts)
if (opts.json) {
return JSON.stringify(processedConfig, null, 2)
}
return encode(processedConfig)
export type ConfigListOptions = Pick<ConfigCommandOptions, 'rawConfig'>
export async function configList (opts: ConfigListOptions): Promise<string> {
const processedConfig = processConfig(opts.rawConfig)
return JSON.stringify(processedConfig, undefined, 2)
}

View File

@@ -17,10 +17,6 @@ export interface ProcessConfigOptions {
json?: boolean
}
function normalizeConfigKeyCases (rawConfig: Record<string, unknown>, opts?: ProcessConfigOptions): Record<string, unknown> {
return opts?.json ? camelCaseConfig(rawConfig) : rawConfig
}
export function processConfig (rawConfig: Record<string, unknown>, opts?: ProcessConfigOptions): Record<string, unknown> {
return normalizeConfigKeyCases(censorProtectedSettings(sortDirectKeys(rawConfig)), opts)
export function processConfig (rawConfig: Record<string, unknown>): Record<string, unknown> {
return camelCaseConfig(censorProtectedSettings(sortDirectKeys(rawConfig)))
}

View File

@@ -1,4 +1,3 @@
import * as ini from 'ini'
import { config } from '@pnpm/plugin-commands-config'
import { getOutputString } from './utils/index.js'
@@ -64,7 +63,7 @@ test('config get on array should return a comma-separated list', async () => {
])
})
test('config get on object should return an ini string', async () => {
test('config get on object should return a JSON string', async () => {
const getResult = await config.handler({
dir: process.cwd(),
cliOptions: {},
@@ -77,7 +76,7 @@ test('config get on object should return an ini string', async () => {
},
}, ['get', 'catalog'])
expect(ini.decode(getOutputString(getResult))).toEqual({ react: '^19.0.0' })
expect(JSON.parse(getOutputString(getResult))).toStrictEqual({ react: '^19.0.0' })
})
test('config get without key show list all settings', async () => {
@@ -100,10 +99,11 @@ test('config get without key show list all settings', async () => {
rawConfig,
}, ['list'])
expect(getOutput).toEqual(listOutput)
expect(getOutput).toStrictEqual(listOutput)
})
describe('config get with a property path', () => {
// TODO: change `rawConfig` into camelCase (to emulate pnpm-workspace.yaml)
const rawConfig = {
'dlx-cache-max-age': '1234',
'only-built-dependencies': ['foo', 'bar'],
@@ -169,7 +169,13 @@ describe('config get with a property path', () => {
describe('object without --json', () => {
test.each([
['', rawConfig],
// TODO: change `rawConfig` into camelCase and replace this object with just `rawConfig`.
['', {
dlxCacheMaxAge: rawConfig['dlx-cache-max-age'],
onlyBuiltDependencies: rawConfig['only-built-dependencies'],
packageExtensions: rawConfig.packageExtensions,
}],
['packageExtensions', rawConfig.packageExtensions],
['packageExtensions["@babel/parser"]', rawConfig.packageExtensions['@babel/parser']],
['packageExtensions["@babel/parser"].peerDependencies', rawConfig.packageExtensions['@babel/parser'].peerDependencies],
@@ -184,7 +190,7 @@ describe('config get with a property path', () => {
rawConfig,
}, ['get', propertyPath])
expect(ini.decode(getOutputString(getResult))).toEqual(expected)
expect(JSON.parse(getOutputString(getResult))).toStrictEqual(expected)
})
})

View File

@@ -1,4 +1,3 @@
import * as ini from 'ini'
import { config } from '@pnpm/plugin-commands-config'
import { getOutputString } from './utils/index.js'
@@ -13,9 +12,9 @@ test('config list', async () => {
},
}, ['list'])
expect(ini.decode(getOutputString(output))).toEqual({
'fetch-retries': '2',
'store-dir': '~/store',
expect(JSON.parse(getOutputString(output))).toStrictEqual({
fetchRetries: '2',
storeDir: '~/store',
})
})
@@ -53,8 +52,10 @@ test('config list censors protected settings', async () => {
rawConfig,
}, ['list'])
expect(ini.decode(getOutputString(output))).toEqual({
...rawConfig,
expect(JSON.parse(getOutputString(output))).toStrictEqual({
storeDir: '~/store',
fetchRetries: '2',
'@my-org:registry': 'https://my-org.example.com/registry',
'//my-org.example.com:username': '(protected)',
username: '(protected)',
})

View File

@@ -1,5 +1,4 @@
import fs from 'fs'
import * as ini from 'ini'
import { sync as writeYamlFile } from 'write-yaml-file'
import { type Config } from '@pnpm/config'
import { prepare } from '@pnpm/prepare'
@@ -109,12 +108,6 @@ test('pnpm config list still reads unknown camelCase keys from pnpm-workspace.ya
{
const { stdout } = execPnpmSync(['config', 'list'], { expectSuccess: true })
expect(ini.decode(stdout.toString())).toMatchObject(workspaceManifest)
expect(ini.decode(stdout.toString())).not.toHaveProperty(['this-option-is-not-defined-by-pnpm'])
}
{
const { stdout } = execPnpmSync(['config', 'list', '--json'], { expectSuccess: true })
expect(JSON.parse(stdout.toString())).toMatchObject(workspaceManifest)
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['this-option-is-not-defined-by-pnpm'])
}
@@ -142,43 +135,9 @@ test('pnpm config list --json shows all keys in camelCase', () => {
prepare()
writeYamlFile('pnpm-workspace.yaml', workspaceManifest)
const { stdout } = execPnpmSync(['config', 'list', '--json'], { expectSuccess: true })
const { stdout } = execPnpmSync(['config', 'list'], { expectSuccess: true })
expect(JSON.parse(stdout.toString())).toStrictEqual(expect.objectContaining(workspaceManifest))
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['dlx-cache-max-age'])
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['only-built-dependencies'])
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['package-extensions'])
})
test('pnpm config list without --json shows rc options in kebab-case and workspace-specific settings in camelCase', () => {
const workspaceManifest = {
dlxCacheMaxAge: 1234,
onlyBuiltDependencies: ['foo', 'bar'],
packages: ['baz', 'qux'],
packageExtensions: {
'@babel/parser': {
peerDependencies: {
'@babel/types': '*',
},
},
'jest-circus': {
dependencies: {
slash: '3',
},
},
},
}
prepare()
writeYamlFile('pnpm-workspace.yaml', workspaceManifest)
const { stdout } = execPnpmSync(['config', 'list'], { expectSuccess: true })
expect(ini.decode(stdout.toString())).toEqual(expect.objectContaining({
'dlx-cache-max-age': String(workspaceManifest.dlxCacheMaxAge), // must be a string because ini doesn't decode to numbers
'only-built-dependencies': workspaceManifest.onlyBuiltDependencies,
packages: workspaceManifest.packages,
packageExtensions: workspaceManifest.packageExtensions,
}))
expect(ini.decode(stdout.toString())).not.toHaveProperty(['dlxCacheMaxAge'])
expect(ini.decode(stdout.toString())).not.toHaveProperty(['onlyBuiltDependencies'])
expect(ini.decode(stdout.toString())).not.toHaveProperty(['package-extensions'])
})