From d4bf2d0e60070b3c6ac895b267e5ae4fc48be5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kh=E1=BA=A3i?= Date: Thu, 30 Oct 2025 17:34:48 +0700 Subject: [PATCH] feat(cli/config)!: config command outputs changed from INI to JSON with camelCase keys (#10142) --- .changeset/crazy-swans-retire.md | 9 ++++ .changeset/sixty-results-end.md | 6 +++ config/plugin-commands-config/src/config.ts | 4 +- .../plugin-commands-config/src/configGet.ts | 9 ++-- .../plugin-commands-config/src/configList.ts | 12 +++--- .../src/processConfig.ts | 8 +--- .../test/configGet.test.ts | 18 +++++--- .../test/configList.test.ts | 13 +++--- pnpm/test/config/list.ts | 43 +------------------ 9 files changed, 47 insertions(+), 75 deletions(-) create mode 100644 .changeset/crazy-swans-retire.md create mode 100644 .changeset/sixty-results-end.md diff --git a/.changeset/crazy-swans-retire.md b/.changeset/crazy-swans-retire.md new file mode 100644 index 0000000000..46423fdac6 --- /dev/null +++ b/.changeset/crazy-swans-retire.md @@ -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. diff --git a/.changeset/sixty-results-end.md b/.changeset/sixty-results-end.md new file mode 100644 index 0000000000..89d0d5e66a --- /dev/null +++ b/.changeset/sixty-results-end.md @@ -0,0 +1,6 @@ +--- +"@pnpm/plugin-commands-config": major +"pnpm": major +--- + +`pnpm config list` now prints a JSON object instead of INI formatted text. diff --git a/config/plugin-commands-config/src/config.ts b/config/plugin-commands-config/src/config.ts index 71362035c2..4dc9e3f6f9 100644 --- a/config/plugin-commands-config/src/config.ts +++ b/config/plugin-commands-config/src/config.ts @@ -58,7 +58,7 @@ export function help (): string { name: '--location ', }, { - 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 ', 'pnpm config get ', + 'pnpm config get --json ', 'pnpm config delete ', 'pnpm config list', - 'pnpm config list --json', ], }) } diff --git a/config/plugin-commands-config/src/configGet.ts b/config/plugin-commands-config/src/configGet.ts index b293fc248a..0877d72fdc 100644 --- a/config/plugin-commands-config/src/configGet.ts +++ b/config/plugin-commands-config/src/configGet.ts @@ -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, key: string, isScopedK return undefined } -type GetConfigByPropertyPathOptions = Pick - -function getConfigByPropertyPath (rawConfig: Record, propertyPath: string, opts?: GetConfigByPropertyPathOptions): Found { +function getConfigByPropertyPath (rawConfig: Record, propertyPath: string): Found { 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) } diff --git a/config/plugin-commands-config/src/configList.ts b/config/plugin-commands-config/src/configList.ts index 64d3e24d62..6e82c31647 100644 --- a/config/plugin-commands-config/src/configList.ts +++ b/config/plugin-commands-config/src/configList.ts @@ -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 { - const processedConfig = processConfig(opts.rawConfig, opts) - if (opts.json) { - return JSON.stringify(processedConfig, null, 2) - } - return encode(processedConfig) +export type ConfigListOptions = Pick + +export async function configList (opts: ConfigListOptions): Promise { + const processedConfig = processConfig(opts.rawConfig) + return JSON.stringify(processedConfig, undefined, 2) } diff --git a/config/plugin-commands-config/src/processConfig.ts b/config/plugin-commands-config/src/processConfig.ts index 4ab709020a..3acc48e409 100644 --- a/config/plugin-commands-config/src/processConfig.ts +++ b/config/plugin-commands-config/src/processConfig.ts @@ -17,10 +17,6 @@ export interface ProcessConfigOptions { json?: boolean } -function normalizeConfigKeyCases (rawConfig: Record, opts?: ProcessConfigOptions): Record { - return opts?.json ? camelCaseConfig(rawConfig) : rawConfig -} - -export function processConfig (rawConfig: Record, opts?: ProcessConfigOptions): Record { - return normalizeConfigKeyCases(censorProtectedSettings(sortDirectKeys(rawConfig)), opts) +export function processConfig (rawConfig: Record): Record { + return camelCaseConfig(censorProtectedSettings(sortDirectKeys(rawConfig))) } diff --git a/config/plugin-commands-config/test/configGet.test.ts b/config/plugin-commands-config/test/configGet.test.ts index 5c9706d36c..35ca8a222f 100644 --- a/config/plugin-commands-config/test/configGet.test.ts +++ b/config/plugin-commands-config/test/configGet.test.ts @@ -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) }) }) diff --git a/config/plugin-commands-config/test/configList.test.ts b/config/plugin-commands-config/test/configList.test.ts index 3ad66c806b..0958e21c9d 100644 --- a/config/plugin-commands-config/test/configList.test.ts +++ b/config/plugin-commands-config/test/configList.test.ts @@ -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)', }) diff --git a/pnpm/test/config/list.ts b/pnpm/test/config/list.ts index 21d96f1c28..e8b2e3f97c 100644 --- a/pnpm/test/config/list.ts +++ b/pnpm/test/config/list.ts @@ -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']) -})