Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement the config commands #5829

Merged
merged 18 commits into from Dec 24, 2022
Merged
16 changes: 16 additions & 0 deletions .changeset/perfect-squids-count.md
@@ -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)
6 changes: 6 additions & 0 deletions .changeset/tiny-chicken-train.md
@@ -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).
5 changes: 0 additions & 5 deletions __typings__/typed.d.ts
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions config/config/package.json
Expand Up @@ -37,7 +37,7 @@
"@pnpm/error": "workspace:*",
"@pnpm/git-utils": "workspace:*",
"@pnpm/matcher": "workspace:*",
"@pnpm/npm-conf": "2.0.4",
"@pnpm/npm-conf": "2.0.0",
"@pnpm/pnpmfile": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"@pnpm/types": "workspace:*",
Expand All @@ -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"
},
Expand Down
14 changes: 10 additions & 4 deletions config/config/src/index.ts
Expand Up @@ -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

Expand All @@ -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, [
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion config/config/src/readLocalConfig.ts
@@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions config/plugin-commands-config/README.md
@@ -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
3 changes: 3 additions & 0 deletions config/plugin-commands-config/jest.config.js
@@ -0,0 +1,3 @@
const config = require('../../jest.config.js')

module.exports = config
52 changes: 52 additions & 0 deletions config/plugin-commands-config/package.json
@@ -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"
}
}
8 changes: 8 additions & 0 deletions config/plugin-commands-config/src/ConfigCommandOptions.ts
@@ -0,0 +1,8 @@
import { Config } from '@pnpm/config'

export type ConfigCommandOptions = Pick<Config,
| 'configDir'
| 'dir'
| 'global'
| 'rawConfig'
> & { json?: boolean }
91 changes: 91 additions & 0 deletions config/plugin-commands-config/src/config.ts
@@ -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')
}
}
}
5 changes: 5 additions & 0 deletions config/plugin-commands-config/src/configGet.ts
@@ -0,0 +1,5 @@
import { ConfigCommandOptions } from './ConfigCommandOptions'

export function configGet (opts: ConfigCommandOptions, key: string) {
return opts.rawConfig[key]
}
11 changes: 11 additions & 0 deletions config/plugin-commands-config/src/configList.ts
@@ -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)
}
25 changes: 25 additions & 0 deletions config/plugin-commands-config/src/configSet.ts
@@ -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
}
}
12 changes: 12 additions & 0 deletions config/plugin-commands-config/src/get.ts
@@ -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])
}
5 changes: 5 additions & 0 deletions config/plugin-commands-config/src/index.ts
@@ -0,0 +1,5 @@
import * as config from './config'
import * as getCommand from './get'
import * as setCommand from './set'

export { config, getCommand, setCommand }
12 changes: 12 additions & 0 deletions config/plugin-commands-config/src/set.ts
@@ -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])
}
24 changes: 24 additions & 0 deletions config/plugin-commands-config/test/configDelete.test.ts
@@ -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',
})
})
14 changes: 14 additions & 0 deletions config/plugin-commands-config/test/configGet.test.ts
@@ -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')
})