feat: implement the config commands (#5829)

close #5621
This commit is contained in:
Zoltan Kochan
2022-12-24 17:08:43 +02:00
committed by GitHub
parent 4700b095ec
commit 841f52e709
29 changed files with 492 additions and 25 deletions

View File

@@ -0,0 +1,16 @@
---
"@pnpm/plugin-commands-config": major
"pnpm": minor
---
pnpm gets its own implementation of the following commands:
* `pnpm config get`
* `pnpm config set`
* `pnpm config delete`
* `pnpm config list`
In previous versions these commands were passing through to npm CLI.
PR: [#5829](https://github.com/pnpm/pnpm/pull/5829)
Related issue: [#5621](https://github.com/pnpm/pnpm/issues/5621)

View File

@@ -0,0 +1,6 @@
---
"@pnpm/config": minor
"pnpm": minor
---
pnpm reads settings from its own global configuration file at `$XDG_CONFIG_HOME/pnpm/rc` [#5829](https://github.com/pnpm/pnpm/pull/5829).

View File

@@ -20,11 +20,6 @@ declare module 'path-name' {
export = pathname;
}
declare module 'read-ini-file' {
function readIniFile (filename: string): Promise<Object>;
export = readIniFile;
}
declare module 'right-pad' {
function rightPad (txt: string, size: number): string;
export = rightPad;

View File

@@ -50,7 +50,7 @@
"path-absolute": "^1.0.1",
"path-name": "^1.0.0",
"ramda": "npm:@pnpm/ramda@0.28.1",
"read-ini-file": "^3.1.0",
"read-ini-file": "4.0.0",
"realpath-missing": "^1.1.0",
"which": "^3.0.0"
},

View File

@@ -235,7 +235,15 @@ export async function getConfig (
'registry-supports-time-field': false,
})
npmConfig.addFile(path.resolve(path.join(__dirname, 'pnpmrc')), 'pnpm-builtin')
const configDir = getConfigDir(process)
{
const warn = npmConfig.addFile(path.join(configDir as string, 'rc'), 'pnpm-global')
if (warn) warnings.push(warn)
}
{
const warn = npmConfig.addFile(path.resolve(path.join(__dirname, 'pnpmrc')), 'pnpm-builtin')
if (warn) warnings.push(warn)
}
delete cliOptions.prefix
@@ -252,6 +260,7 @@ export async function getConfig (
pnpmConfig.maxSockets = npmConfig.maxsockets
delete pnpmConfig['maxsockets']
pnpmConfig.configDir = configDir
pnpmConfig.workspaceDir = opts.workspaceDir
pnpmConfig.workspaceRoot = cliOptions['workspace-root'] as boolean // This is needed to prevent pnpm reading workspaceRoot from env variables
pnpmConfig.rawLocalConfig = Object.assign.apply(Object, [
@@ -428,9 +437,6 @@ export async function getConfig (
if (!pnpmConfig.stateDir) {
pnpmConfig.stateDir = getStateDir(process)
}
if (!pnpmConfig.configDir) {
pnpmConfig.configDir = getConfigDir(process)
}
if (pnpmConfig['hoist'] === false) {
delete pnpmConfig.hoistPattern
}

View File

@@ -1,7 +1,7 @@
import path from 'path'
import camelcaseKeys from 'camelcase-keys'
import { envReplace } from '@pnpm/config.env-replace'
import readIniFile from 'read-ini-file'
import { readIniFile } from 'read-ini-file'
export async function readLocalConfig (prefix: string) {
try {

View File

@@ -0,0 +1,15 @@
# @pnpm/plugin-commands-config
> Commands for reading and writing settings to/from config files
[![npm version](https://img.shields.io/npm/v/@pnpm/plugin-commands-config.svg)](https://www.npmjs.com/package/@pnpm/plugin-commands-config)
## Installation
```sh
pnpm add @pnpm/plugin-commands-config
```
## License
MIT

View File

@@ -0,0 +1,3 @@
const config = require('../../jest.config.js')
module.exports = config

View File

@@ -0,0 +1,52 @@
{
"name": "@pnpm/plugin-commands-config",
"version": "0.0.0",
"description": "Commands for reading and writing settings to/from config files",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"!*.map"
],
"engines": {
"node": ">=14.6"
},
"scripts": {
"lint": "eslint src/**/*.ts test/**/*.ts",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"repository": "https://github.com/pnpm/pnpm/blob/main/config/plugin-commands-config",
"keywords": [
"pnpm7",
"pnpm",
"config"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/config/plugin-commands-config#readme",
"dependencies": {
"@pnpm/cli-utils": "workspace:*",
"@pnpm/config": "workspace:*",
"@pnpm/error": "workspace:*",
"ini": "3.0.1",
"read-ini-file": "4.0.0",
"render-help": "^1.0.2",
"sort-keys": "^4.2.0",
"write-ini-file": "4.0.1"
},
"funding": "https://opencollective.com/pnpm",
"devDependencies": {
"@pnpm/logger": "^5.0.0",
"@pnpm/plugin-commands-config": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@types/ini": "1.3.31"
},
"exports": {
".": "./lib/index.js"
}
}

View File

@@ -0,0 +1,8 @@
import { Config } from '@pnpm/config'
export type ConfigCommandOptions = Pick<Config,
| 'configDir'
| 'dir'
| 'global'
| 'rawConfig'
> & { json?: boolean }

View File

@@ -0,0 +1,91 @@
import { docsUrl } from '@pnpm/cli-utils'
import { PnpmError } from '@pnpm/error'
import renderHelp from 'render-help'
import { configGet } from './configGet'
import { configSet } from './configSet'
import { configList } from './configList'
import { ConfigCommandOptions } from './ConfigCommandOptions'
export function rcOptionsTypes () {
return {}
}
export function cliOptionsTypes () {
return {
global: Boolean,
json: Boolean,
}
}
export const commandNames = ['config', 'c']
export function help () {
return renderHelp({
description: 'Manage the pnpm configuration files.',
descriptionLists: [
{
title: 'Commands',
list: [
{
description: 'Set the config key to the value provided',
name: 'set',
},
{
description: 'Print the config value for the provided key',
name: 'get',
},
{
description: 'Remove the config key from the config file',
name: 'delete',
},
{
description: 'Show all the config settings',
name: 'list',
},
],
},
{
title: 'Options',
list: [
{
description: 'Sets the configuration in the global config file',
name: '--global',
shortAlias: '-g',
},
],
},
],
url: docsUrl('config'),
usages: [
'pnpm config set <key> <value>',
'pnpm config get <key>',
'pnpm config delete <key>',
'pnpm config list',
],
})
}
export async function handler (opts: ConfigCommandOptions, params: string[]) {
if (params.length === 0) {
throw new PnpmError('CONFIG_NO_SUBCOMMAND', 'Please specify the subcommand', {
hint: help(),
})
}
switch (params[0]) {
case 'set': {
return configSet(opts, params[1], params[2] ?? '')
}
case 'get': {
return configGet(opts, params[1])
}
case 'delete': {
return configSet(opts, params[1], null)
}
case 'list': {
return configList(opts)
}
default: {
throw new PnpmError('CONFIG_UNKNOWN_SUBCOMMAND', 'This subcommand is not known')
}
}
}

View File

@@ -0,0 +1,5 @@
import { ConfigCommandOptions } from './ConfigCommandOptions'
export function configGet (opts: ConfigCommandOptions, key: string) {
return opts.rawConfig[key]
}

View File

@@ -0,0 +1,11 @@
import { encode } from 'ini'
import sortKeys from 'sort-keys'
import { ConfigCommandOptions } from './ConfigCommandOptions'
export async function configList (opts: ConfigCommandOptions) {
const sortedConfig = sortKeys(opts.rawConfig)
if (opts.json) {
return JSON.stringify(sortedConfig, null, 2)
}
return encode(sortedConfig)
}

View File

@@ -0,0 +1,25 @@
import path from 'path'
import { readIniFile } from 'read-ini-file'
import { writeIniFile } from 'write-ini-file'
import { ConfigCommandOptions } from './ConfigCommandOptions'
export async function configSet (opts: ConfigCommandOptions, key: string, value: string | null) {
const configPath = opts.global ? path.join(opts.configDir, 'rc') : path.join(opts.dir, '.npmrc')
const settings = await safeReadIniFile(configPath)
if (value == null) {
if (settings[key] == null) return
delete settings[key]
} else {
settings[key] = value
}
await writeIniFile(configPath, settings)
}
async function safeReadIniFile (configPath: string) {
try {
return await readIniFile(configPath)
} catch (err: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
if (err.code === 'ENOENT') return {}
throw err
}
}

View File

@@ -0,0 +1,12 @@
import * as configCmd from './config'
import { ConfigCommandOptions } from './ConfigCommandOptions'
export const rcOptionsTypes = configCmd.rcOptionsTypes
export const cliOptionsTypes = configCmd.cliOptionsTypes
export const help = configCmd.help
export const commandNames = ['get']
export async function handler (opts: ConfigCommandOptions, params: string[]) {
return configCmd.handler(opts, ['get', ...params])
}

View File

@@ -0,0 +1,5 @@
import * as config from './config'
import * as getCommand from './get'
import * as setCommand from './set'
export { config, getCommand, setCommand }

View File

@@ -0,0 +1,12 @@
import * as configCmd from './config'
import { ConfigCommandOptions } from './ConfigCommandOptions'
export const rcOptionsTypes = configCmd.rcOptionsTypes
export const cliOptionsTypes = configCmd.cliOptionsTypes
export const help = configCmd.help
export const commandNames = ['set']
export async function handler (opts: ConfigCommandOptions, params: string[]) {
return configCmd.handler(opts, ['set', ...params])
}

View File

@@ -0,0 +1,24 @@
import fs from 'fs'
import path from 'path'
import { tempDir } from '@pnpm/prepare'
import { config } from '@pnpm/plugin-commands-config'
import { readIniFileSync } from 'read-ini-file'
test('config delete', async () => {
const tmp = tempDir()
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(configDir, 'rc'), `store-dir=~/store
cache-dir=~/cache`)
await config.handler({
dir: process.cwd(),
configDir,
global: true,
rawConfig: {},
}, ['delete', 'store-dir'])
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({
'cache-dir': '~/cache',
})
})

View File

@@ -0,0 +1,14 @@
import { config } from '@pnpm/plugin-commands-config'
test('config get', async () => {
const configKey = await config.handler({
dir: process.cwd(),
configDir: process.cwd(),
global: true,
rawConfig: {
'store-dir': '~/store',
},
}, ['get', 'store-dir'])
expect(configKey).toEqual('~/store')
})

View File

@@ -0,0 +1,39 @@
import { config } from '@pnpm/plugin-commands-config'
const CRLF = '\r\n'
function normalizeNewlines (str: string) {
return str.replace(new RegExp(CRLF, 'g'), '\n')
}
test('config list', async () => {
const output = await config.handler({
dir: process.cwd(),
configDir: process.cwd(),
rawConfig: {
'store-dir': '~/store',
'fetch-retries': '2',
},
}, ['list'])
expect(normalizeNewlines(output)).toEqual(`fetch-retries=2
store-dir=~/store
`)
})
test('config list --json', async () => {
const output = await config.handler({
dir: process.cwd(),
configDir: process.cwd(),
json: true,
rawConfig: {
'store-dir': '~/store',
'fetch-retries': '2',
},
}, ['list'])
expect(output).toEqual(JSON.stringify({
'fetch-retries': '2',
'store-dir': '~/store',
}, null, 2))
})

View File

@@ -0,0 +1,24 @@
import fs from 'fs'
import path from 'path'
import { tempDir } from '@pnpm/prepare'
import { config } from '@pnpm/plugin-commands-config'
import { readIniFileSync } from 'read-ini-file'
test('config set', async () => {
const tmp = tempDir()
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(configDir, 'rc'), 'store-dir=~/store')
await config.handler({
dir: process.cwd(),
configDir,
global: true,
rawConfig: {},
}, ['set', 'fetch-retries', '1'])
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({
'store-dir': '~/store',
'fetch-retries': '1',
})
})

View File

@@ -0,0 +1,25 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../__utils__/prepare"
},
{
"path": "../../cli/cli-utils"
},
{
"path": "../../packages/error"
},
{
"path": "../config"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../__typings__/**/*.d.ts"
]
}

80
pnpm-lock.yaml generated
View File

@@ -580,8 +580,8 @@ importers:
specifier: npm:@pnpm/ramda@0.28.1
version: /@pnpm/ramda/0.28.1
read-ini-file:
specifier: ^3.1.0
version: 3.1.0
specifier: 4.0.0
version: 4.0.0
realpath-missing:
specifier: ^1.1.0
version: 1.1.0
@@ -697,6 +697,46 @@ importers:
specifier: workspace:*
version: 'link:'
config/plugin-commands-config:
dependencies:
'@pnpm/cli-utils':
specifier: workspace:*
version: link:../../cli/cli-utils
'@pnpm/config':
specifier: workspace:*
version: link:../config
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
ini:
specifier: 3.0.1
version: 3.0.1
read-ini-file:
specifier: 4.0.0
version: 4.0.0
render-help:
specifier: ^1.0.2
version: 1.0.2
sort-keys:
specifier: ^4.2.0
version: 4.2.0
write-ini-file:
specifier: 4.0.1
version: 4.0.1
devDependencies:
'@pnpm/logger':
specifier: ^5.0.0
version: 5.0.0
'@pnpm/plugin-commands-config':
specifier: workspace:*
version: 'link:'
'@pnpm/prepare':
specifier: workspace:*
version: link:../../__utils__/prepare
'@types/ini':
specifier: 1.3.31
version: 1.3.31
env/node.fetcher:
dependencies:
'@pnpm/create-cafs-store':
@@ -3956,6 +3996,9 @@ importers:
'@pnpm/plugin-commands-audit':
specifier: workspace:*
version: link:../lockfile/plugin-commands-audit
'@pnpm/plugin-commands-config':
specifier: workspace:*
version: link:../config/plugin-commands-config
'@pnpm/plugin-commands-deploy':
specifier: workspace:*
version: link:../releasing/plugin-commands-deploy
@@ -8523,6 +8566,10 @@ packages:
/@types/http-cache-semantics/4.0.1:
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
/@types/ini/1.3.31:
resolution: {integrity: sha512-8ecxxaG4AlVEM1k9+BsziMw8UsX0qy3jYI1ad/71RrDZ+rdL6aZB0wLfAuflQiDhkD5o4yJ0uPK3OSUic3fG0w==}
dev: true
/@types/is-ci/3.0.0:
resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==}
dependencies:
@@ -12357,6 +12404,12 @@ packages:
/ini/2.0.0:
resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==}
engines: {node: '>=10'}
dev: true
/ini/3.0.1:
resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
dev: false
/inquirer/6.5.2:
resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==}
@@ -15209,6 +15262,15 @@ packages:
dependencies:
ini: 2.0.0
strip-bom: 4.0.0
dev: true
/read-ini-file/4.0.0:
resolution: {integrity: sha512-zz4qv/sKETv7nAkATqSJ9YMbKD8NXRPuA8d17VdYCuNYrVstB1S6UAMU6aytf5vRa9MESbZN7jLZdcmrOxz4gg==}
engines: {node: '>=14.6'}
dependencies:
ini: 3.0.1
strip-bom: 4.0.0
dev: false
/read-pkg-up/7.0.1:
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
@@ -17194,6 +17256,15 @@ packages:
imurmurhash: 0.1.4
signal-exit: 3.0.7
/write-ini-file/4.0.1:
resolution: {integrity: sha512-8XPBFS/EqPls2V4vYSc8kPVXjLg2b0GgVVC52w2WEH4sVDXew9rgch60ckXLiTfiYQKmkxezRaRctbGQr7oj5Q==}
engines: {node: '>=14.6'}
dependencies:
ini: 3.0.1
make-dir: 3.1.0
write-file-atomic: 5.0.0
dev: false
/write-json-file/3.2.0:
resolution: {integrity: sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ==}
engines: {node: '>=6'}
@@ -17427,6 +17498,7 @@ time:
/@types/fs-extra/9.0.13: '2021-09-21T19:02:27.512Z'
/@types/graceful-fs/4.1.5: '2021-02-11T21:49:02.172Z'
/@types/hosted-git-info/3.0.2: '2021-07-06T21:35:19.353Z'
/@types/ini/1.3.31: '2021-10-07T21:01:49.672Z'
/@types/is-ci/3.0.0: '2021-03-07T11:49:37.729Z'
/@types/is-windows/1.0.0: '2019-11-19T19:37:55.992Z'
/@types/isexe/2.0.1: '2021-07-06T21:42:29.330Z'
@@ -17524,6 +17596,7 @@ time:
/graceful-git/3.1.2: '2021-09-16T00:23:26.185Z'
/husky/8.0.2: '2022-11-08T03:41:05.629Z'
/hyperdrive-schemas/2.0.0: '2020-07-14T11:16:33.671Z'
/ini/3.0.1: '2022-08-22T17:22:43.830Z'
/is-ci/3.0.1: '2021-10-26T04:02:03.835Z'
/is-inner-link/4.0.0: '2021-02-11T22:54:33.386Z'
/is-port-reachable/3.0.0: '2019-11-12T09:49:42.096Z'
@@ -17578,7 +17651,7 @@ time:
/proxyquire/2.1.3: '2019-08-12T13:54:46.049Z'
/ps-list/7.2.0: '2020-06-17T09:02:36.119Z'
/publish-packed/4.1.1: '2022-01-04T09:56:54.476Z'
/read-ini-file/3.1.0: '2021-02-11T22:53:37.619Z'
/read-ini-file/4.0.0: '2022-12-23T20:19:57.971Z'
/read-yaml-file/2.1.0: '2021-02-11T22:53:46.064Z'
/realpath-missing/1.1.0: '2021-02-11T22:53:50.718Z'
/remark-parse/9.0.0: '2020-10-14T08:48:35.392Z'
@@ -17630,6 +17703,7 @@ time:
/which/3.0.0: '2022-11-01T19:20:24.475Z'
/wrap-ansi/7.0.0: '2020-04-22T16:53:23.889Z'
/write-file-atomic/5.0.0: '2022-10-14T05:22:38.937Z'
/write-ini-file/4.0.1: '2022-12-23T20:19:59.977Z'
/write-json-file/4.3.0: '2020-02-07T08:54:49.528Z'
/write-json5-file/3.1.0: '2021-02-11T22:54:24.439Z'
/write-pkg/4.0.0: '2019-04-29T10:37:09.855Z'

View File

@@ -41,6 +41,7 @@
"@pnpm/parse-cli-args": "workspace:*",
"@pnpm/pick-registry-for-package": "workspace:*",
"@pnpm/plugin-commands-audit": "workspace:*",
"@pnpm/plugin-commands-config": "workspace:*",
"@pnpm/plugin-commands-deploy": "workspace:*",
"@pnpm/plugin-commands-doctor": "workspace:*",
"@pnpm/plugin-commands-env": "workspace:*",

View File

@@ -1,6 +1,7 @@
import { CompletionFunc } from '@pnpm/command'
import { types as allTypes } from '@pnpm/config'
import { audit } from '@pnpm/plugin-commands-audit'
import { config, getCommand, setCommand } from '@pnpm/plugin-commands-config'
import { doctor } from '@pnpm/plugin-commands-doctor'
import { env } from '@pnpm/plugin-commands-env'
import { deploy } from '@pnpm/plugin-commands-deploy'
@@ -99,6 +100,9 @@ const commands: CommandDefinition[] = [
add,
audit,
bin,
config,
getCommand,
setCommand,
create,
deploy,
dlx,

View File

@@ -28,13 +28,10 @@ const argv = process.argv.slice(2)
case 'access':
case 'adduser':
case 'bugs':
case 'c':
case 'config':
case 'deprecate':
case 'dist-tag':
case 'docs':
case 'edit':
case 'get':
case 'info':
case 'login':
case 'logout':
@@ -47,7 +44,6 @@ const argv = process.argv.slice(2)
case 's':
case 'se':
case 'search':
case 'set':
case 'set-script':
case 'star':
case 'stars':

View File

@@ -50,13 +50,6 @@ test('pnpm import does not move modules created by npm', async () => {
expect(packageManifestInodeBefore).toBe(packageManifestInodeAfter)
})
test('pass through to npm CLI for commands that are not supported by npm', () => {
const result = execPnpmSync(['config', 'get', 'user-agent'])
expect(result.status).toBe(0)
expect(result.stdout.toString()).toMatch(/npm\//) // command returned correct result
})
test('pass through to npm with all the args', async () => {
prepare()
await rimraf('package.json')

View File

@@ -45,6 +45,9 @@
{
"path": "../config/pick-registry-for-package"
},
{
"path": "../config/plugin-commands-config"
},
{
"path": "../env/plugin-commands-env"
},