mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-27 18:46:18 -04:00
refactor(config): stop shelling out to npm for auth settings (#11146)
* refactor(config): stop shelling out to npm for auth settings Read and write auth-related settings (registry, tokens, credentials, scoped registries) directly to INI config files instead of delegating to `npm config`. Removes the @pnpm/exec.run-npm dependency from @pnpm/config.commands. * fix(config): give pnpm global rc priority over ~/.npmrc for auth settings Auth settings from the pnpm global rc file (e.g. ~/.config/pnpm/rc) now override ~/.npmrc in rawConfig. This ensures tokens written by `pnpm login` are correctly picked up by `pnpm publish`, since login writes to the pnpm global rc but ~/.npmrc previously took priority in the npm-conf chain. * chore: remove @pnpm/exec.run-npm package No longer used after removing npm config CLI delegation. * chore: remove accidentally committed __typecheck__/tsconfig.json * fix(config): narrow non-string rejection to credential keys, add priority test Non-string value rejection now only applies to credential keys (_auth, _authToken, _password, username), registry URLs, and scoped/registry- prefixed keys — not to INI settings like strict-ssl, proxy, or ca that can legitimately have boolean/null values. Added a test verifying that auth tokens from the pnpm global rc take priority over ~/.npmrc.
This commit is contained in:
9
.changeset/stop-using-npm-config-for-auth.md
Normal file
9
.changeset/stop-using-npm-config-for-auth.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@pnpm/config.commands": minor
|
||||
"@pnpm/config.reader": patch
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
`pnpm config get/set/delete` no longer shells out to `npm config` for auth-related settings. Auth settings (registry, tokens, credentials, scoped registries) are now read from and written to the INI config files directly.
|
||||
|
||||
Auth settings from the pnpm global rc file (`~/.config/pnpm/rc`) now take priority over `~/.npmrc`, so tokens written by `pnpm login` are correctly picked up by `pnpm publish`.
|
||||
@@ -36,7 +36,6 @@
|
||||
"@pnpm/config.reader": "workspace:*",
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/exec.run-npm": "workspace:*",
|
||||
"@pnpm/object.key-sorting": "workspace:*",
|
||||
"@pnpm/object.property-path": "workspace:*",
|
||||
"@pnpm/text.naming-cases": "workspace:*",
|
||||
|
||||
@@ -5,7 +5,6 @@ export type ConfigCommandOptions = Pick<Config,
|
||||
| 'cliOptions'
|
||||
| 'dir'
|
||||
| 'global'
|
||||
| 'npmPath'
|
||||
| 'rawConfig'
|
||||
| 'workspaceDir'
|
||||
> & {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { types } from '@pnpm/config.reader'
|
||||
import { runNpm } from '@pnpm/exec.run-npm'
|
||||
import { isIniConfigKey, types } from '@pnpm/config.reader'
|
||||
import { getObjectValueByPropertyPath } from '@pnpm/object.property-path'
|
||||
import { isCamelCase, isStrictlyKebabCase } from '@pnpm/text.naming-cases'
|
||||
import kebabCase from 'lodash.kebabcase'
|
||||
@@ -9,19 +6,9 @@ import kebabCase from 'lodash.kebabcase'
|
||||
import type { ConfigCommandOptions } from './ConfigCommandOptions.js'
|
||||
import { parseConfigPropertyPath } from './parseConfigPropertyPath.js'
|
||||
import { processConfig } from './processConfig.js'
|
||||
import { settingShouldFallBackToNpm } from './settingShouldFallBackToNpm.js'
|
||||
|
||||
export function configGet (opts: ConfigCommandOptions, key: string): { output: string, exitCode: number } {
|
||||
const isScopedKey = key.startsWith('@')
|
||||
// Exclude scoped keys from npm fallback because they are pnpm-native config
|
||||
// that can be read directly from rawConfig (e.g., '@scope:registry')
|
||||
if (opts.global && settingShouldFallBackToNpm(key) && !isScopedKey) {
|
||||
const { status: exitCode } = runNpm(opts.npmPath, ['config', 'get', key], {
|
||||
location: 'user',
|
||||
userConfigPath: path.join(opts.configDir, 'rc'),
|
||||
})
|
||||
return { output: '', exitCode: exitCode ?? 0 }
|
||||
}
|
||||
const configResult = getRcConfig(opts.rawConfig, key, isScopedKey) ?? getConfigByPropertyPath(opts.rawConfig, key)
|
||||
const output = displayConfig(configResult?.value, opts)
|
||||
return { output, exitCode: 0 }
|
||||
@@ -45,6 +32,10 @@ function getRcConfig (rawConfig: Record<string, unknown>, key: string, isScopedK
|
||||
const value = rawConfig[key]
|
||||
return { value }
|
||||
}
|
||||
if (isIniConfigKey(key)) {
|
||||
const value = rawConfig[key]
|
||||
return { value }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import path from 'node:path'
|
||||
import util from 'node:util'
|
||||
|
||||
import { type ConfigFileKey, isConfigFileKey, types } from '@pnpm/config.reader'
|
||||
import { type ConfigFileKey, isConfigFileKey, isIniConfigKey, types } from '@pnpm/config.reader'
|
||||
import { GLOBAL_CONFIG_YAML_FILENAME, WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { runNpm, type RunNPMOptions } from '@pnpm/exec.run-npm'
|
||||
import { parsePropertyPath } from '@pnpm/object.property-path'
|
||||
import { isCamelCase, isStrictlyKebabCase } from '@pnpm/text.naming-cases'
|
||||
import { updateWorkspaceManifest } from '@pnpm/workspace.workspace-manifest-writer'
|
||||
@@ -15,48 +14,34 @@ import { writeIniFile } from 'write-ini-file'
|
||||
|
||||
import type { ConfigCommandOptions } from './ConfigCommandOptions.js'
|
||||
import { getConfigFileInfo } from './getConfigFileInfo.js'
|
||||
import { settingShouldFallBackToNpm } from './settingShouldFallBackToNpm.js'
|
||||
|
||||
export async function configSet (opts: ConfigCommandOptions, key: string, valueParam: string | null): Promise<void> {
|
||||
let shouldFallbackToNpm = settingShouldFallBackToNpm(key)
|
||||
if (!shouldFallbackToNpm) {
|
||||
let isAuthSetting = isIniConfigKey(key)
|
||||
if (!isAuthSetting) {
|
||||
key = validateSimpleKey(key)
|
||||
shouldFallbackToNpm = settingShouldFallBackToNpm(key)
|
||||
isAuthSetting = isIniConfigKey(key)
|
||||
}
|
||||
let value: unknown = valueParam
|
||||
if (valueParam != null && opts.json) {
|
||||
value = JSON.parse(valueParam)
|
||||
}
|
||||
|
||||
if (shouldFallbackToNpm) {
|
||||
if (opts.global) {
|
||||
const configPath = path.join(opts.configDir, 'rc')
|
||||
const runNpmOpts: RunNPMOptions = {
|
||||
location: 'user',
|
||||
userConfigPath: configPath,
|
||||
}
|
||||
const _runNpm = runNpm.bind(null, opts.npmPath)
|
||||
if (value == null) {
|
||||
_runNpm(['config', 'delete', key], runNpmOpts)
|
||||
return
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
_runNpm(['config', 'set', `${key}=${value}`], runNpmOpts)
|
||||
return
|
||||
}
|
||||
if (isAuthSetting) {
|
||||
const configPath = opts.global
|
||||
? path.join(opts.configDir, 'rc')
|
||||
: path.join(opts.dir, '.npmrc')
|
||||
if (value != null && typeof value !== 'string' && isStringOnlyIniKey(key)) {
|
||||
throw new PnpmError('CONFIG_SET_AUTH_NON_STRING', `Cannot set ${key} to a non-string value (${JSON.stringify(value)})`)
|
||||
} else {
|
||||
const configPath = 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)
|
||||
return
|
||||
}
|
||||
const settings = await safeReadIniFile(configPath)
|
||||
if (value == null) {
|
||||
if (settings[key] == null) return
|
||||
delete settings[key]
|
||||
} else {
|
||||
settings[key] = value
|
||||
}
|
||||
await writeIniFile(configPath, settings)
|
||||
return
|
||||
}
|
||||
|
||||
const { configDir, configFileName } = getConfigFileInfo(key, opts)
|
||||
@@ -214,6 +199,15 @@ function validateWorkspaceKey (key: string): string {
|
||||
return key
|
||||
}
|
||||
|
||||
const STRING_ONLY_INI_KEYS = ['_auth', '_authToken', '_password', 'username', 'registry']
|
||||
|
||||
function isStringOnlyIniKey (key: string): boolean {
|
||||
if (STRING_ONLY_INI_KEYS.includes(key)) return true
|
||||
if (key.startsWith('@')) return true
|
||||
if (key.startsWith('//')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
async function safeReadIniFile (configPath: string): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
return await readIniFile(configPath) as Record<string, unknown>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// NOTE: The logic may be duplicated with `isIniConfigKey` from `@pnpm/config.reader`,
|
||||
// but we have not the time to refactor it right now.
|
||||
// TODO: Refactor it when we have the time.
|
||||
export function settingShouldFallBackToNpm (key: string): boolean {
|
||||
return (
|
||||
['registry', '_auth', '_authToken', 'username', '_password'].includes(key) ||
|
||||
key[0] === '@' ||
|
||||
key.startsWith('//')
|
||||
)
|
||||
}
|
||||
@@ -40,7 +40,7 @@ test('config delete on registry key set', async () => {
|
||||
rawConfig: {},
|
||||
}, ['delete', 'registry'])
|
||||
|
||||
expect(fs.readdirSync(configDir)).not.toContain('rc')
|
||||
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({})
|
||||
})
|
||||
|
||||
test('config delete on npm-compatible key not set', async () => {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import path from 'node:path'
|
||||
|
||||
jest.unstable_mockModule('@pnpm/exec.run-npm', () => ({
|
||||
runNpm: jest.fn(),
|
||||
}))
|
||||
import { config } from '@pnpm/config.commands'
|
||||
import { tempDir } from '@pnpm/prepare'
|
||||
|
||||
const { config } = await import('@pnpm/config.commands')
|
||||
const { runNpm } = await import('@pnpm/exec.run-npm')
|
||||
import { type ConfigFilesData, readConfigFiles, writeConfigFiles } from './utils/index.js'
|
||||
|
||||
describe.each(
|
||||
[
|
||||
@@ -14,54 +12,165 @@ describe.each(
|
||||
'_password',
|
||||
'username',
|
||||
'registry',
|
||||
'@foo:registry',
|
||||
'//registry.npmjs.org/:_authToken',
|
||||
]
|
||||
)('settings related to auth are handled by npm CLI', (key) => {
|
||||
describe('without --json', () => {
|
||||
const configOpts = {
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir: import.meta.dirname, // this doesn't matter, it won't be used
|
||||
rawConfig: {},
|
||||
}
|
||||
)('auth settings are written to the rc file directly', (key) => {
|
||||
describe('global (without --json)', () => {
|
||||
it(`should set ${key}`, async () => {
|
||||
await config.handler(configOpts, ['set', `${key}=123`])
|
||||
expect(runNpm).toHaveBeenCalledWith(undefined, ['config', 'set', `${key}=123`], expect.objectContaining({
|
||||
location: 'user',
|
||||
userConfigPath: expect.any(String),
|
||||
}))
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: {},
|
||||
globalYaml: undefined,
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: tmp,
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['set', `${key}=123`])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalRc: { [key]: '123' },
|
||||
})
|
||||
})
|
||||
it(`should delete ${key}`, async () => {
|
||||
await config.handler(configOpts, ['delete', key])
|
||||
expect(runNpm).toHaveBeenCalledWith(undefined, ['config', 'delete', key], expect.objectContaining({
|
||||
location: 'user',
|
||||
userConfigPath: expect.any(String),
|
||||
}))
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: { [key]: 'some-value' },
|
||||
globalYaml: undefined,
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: tmp,
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['delete', key])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalRc: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with --json', () => {
|
||||
const configOpts = {
|
||||
json: true,
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir: import.meta.dirname, // this doesn't matter, it won't be used
|
||||
rawConfig: {},
|
||||
}
|
||||
describe('global (with --json)', () => {
|
||||
it(`should set ${key}`, async () => {
|
||||
await config.handler(configOpts, ['set', key, '"123"'])
|
||||
expect(runNpm).toHaveBeenCalledWith(undefined, ['config', 'set', `${key}=123`], expect.objectContaining({
|
||||
location: 'user',
|
||||
userConfigPath: expect.any(String),
|
||||
}))
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: {},
|
||||
globalYaml: undefined,
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
json: true,
|
||||
dir: tmp,
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['set', key, '"123"'])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalRc: { [key]: '123' },
|
||||
})
|
||||
})
|
||||
it(`should delete ${key}`, async () => {
|
||||
await config.handler(configOpts, ['delete', key])
|
||||
expect(runNpm).toHaveBeenCalledWith(undefined, ['config', 'delete', key], expect.objectContaining({
|
||||
location: 'user',
|
||||
userConfigPath: expect.any(String),
|
||||
}))
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: { [key]: 'some-value' },
|
||||
globalYaml: undefined,
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
json: true,
|
||||
dir: tmp,
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['delete', key])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalRc: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe.each(
|
||||
[
|
||||
'@foo:registry',
|
||||
]
|
||||
)('scoped auth settings are written to the rc file directly', (key) => {
|
||||
it(`should set ${key} globally`, async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: {},
|
||||
globalYaml: undefined,
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: tmp,
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['set', `${key}=https://registry.example.com/`])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalRc: { [key]: 'https://registry.example.com/' },
|
||||
})
|
||||
})
|
||||
it(`should delete ${key} globally`, async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: { [key]: 'https://registry.example.com/' },
|
||||
globalYaml: undefined,
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: tmp,
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['delete', key])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalRc: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -77,15 +186,17 @@ describe.each(
|
||||
'//registry.npmjs.org/:_authToken',
|
||||
]
|
||||
)('non-string values should be rejected', (key) => {
|
||||
const configOpts = {
|
||||
json: true,
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir: import.meta.dirname, // this doesn't matter, it won't be used
|
||||
rawConfig: {},
|
||||
}
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
it(`${key} should reject a non-string value`, async () => {
|
||||
await expect(config.handler(configOpts, ['set', key, '{}'])).rejects.toMatchObject({
|
||||
await expect(config.handler({
|
||||
json: true,
|
||||
dir: tmp,
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['set', key, '{}'])).rejects.toMatchObject({
|
||||
code: 'ERR_PNPM_CONFIG_SET_AUTH_NON_STRING',
|
||||
})
|
||||
})
|
||||
@@ -96,25 +207,53 @@ describe.each(
|
||||
'._auth',
|
||||
"['_auth']",
|
||||
]
|
||||
)('%p is handled by npm CLI', (propertyPath) => {
|
||||
const configOpts = {
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir: import.meta.dirname, // this doesn't matter, it won't be used
|
||||
rawConfig: {},
|
||||
}
|
||||
)('%p is handled as an auth setting', (propertyPath) => {
|
||||
it('should set _auth', async () => {
|
||||
await config.handler(configOpts, ['set', propertyPath, '123'])
|
||||
expect(runNpm).toHaveBeenCalledWith(undefined, ['config', 'set', '_auth=123'], expect.objectContaining({
|
||||
location: 'user',
|
||||
userConfigPath: expect.any(String),
|
||||
}))
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: {},
|
||||
globalYaml: undefined,
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: tmp,
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['set', propertyPath, '123'])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalRc: { _auth: '123' },
|
||||
})
|
||||
})
|
||||
it('should delete _auth', async () => {
|
||||
await config.handler(configOpts, ['delete', propertyPath])
|
||||
expect(runNpm).toHaveBeenCalledWith(undefined, ['config', 'delete', '_auth'], expect.objectContaining({
|
||||
location: 'user',
|
||||
userConfigPath: expect.any(String),
|
||||
}))
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: { _auth: 'some-value' },
|
||||
globalYaml: undefined,
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: tmp,
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['delete', propertyPath])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalRc: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,9 +24,6 @@
|
||||
{
|
||||
"path": "../../core/logger"
|
||||
},
|
||||
{
|
||||
"path": "../../exec/run-npm"
|
||||
},
|
||||
{
|
||||
"path": "../../object/key-sorting"
|
||||
},
|
||||
|
||||
@@ -294,9 +294,14 @@ export async function getConfig (opts: {
|
||||
pnpmConfig.userAgent = pnpmConfig.rawLocalConfig['user-agent']
|
||||
? pnpmConfig.rawLocalConfig['user-agent']
|
||||
: `${packageManager.name}/${packageManager.version} npm/? node/${process.version} ${process.platform} ${process.arch}`
|
||||
const pnpmGlobalRcData = npmConfig.sources['pnpm-global']?.data
|
||||
const pnpmGlobalRc = pnpmGlobalRcData
|
||||
? pickIniConfig(Object.fromEntries(Object.entries(pnpmGlobalRcData)))
|
||||
: {}
|
||||
pnpmConfig.rawConfig = Object.assign(
|
||||
{},
|
||||
...npmConfig.list.map(pickIniConfig).reverse(),
|
||||
pnpmGlobalRc,
|
||||
pickIniConfig(cliOptions),
|
||||
{ 'user-agent': pnpmConfig.userAgent },
|
||||
{ globalconfig: path.join(configDir, 'rc') },
|
||||
|
||||
@@ -444,6 +444,47 @@ test('registries in current directory\'s .npmrc have bigger priority then global
|
||||
})
|
||||
})
|
||||
|
||||
test('auth tokens from pnpm global rc override ~/.npmrc', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
// Set up a userconfig (.npmrc) with a stale token
|
||||
fs.writeFileSync('.npmrc', '//registry.npmjs.org/:_authToken=stale-token', 'utf8')
|
||||
|
||||
// Set up a pnpm global rc with a fresh token via XDG_CONFIG_HOME
|
||||
const configHome = path.resolve('xdg-config')
|
||||
fs.mkdirSync(path.join(configHome, 'pnpm'), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(configHome, 'pnpm', 'rc'),
|
||||
'//registry.npmjs.org/:_authToken=fresh-token'
|
||||
)
|
||||
|
||||
const originalXdg = process.env.XDG_CONFIG_HOME
|
||||
process.env.XDG_CONFIG_HOME = configHome
|
||||
try {
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {
|
||||
userconfig: path.resolve('.npmrc'),
|
||||
},
|
||||
env: {
|
||||
...env,
|
||||
XDG_CONFIG_HOME: configHome,
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
expect(config.rawConfig['//registry.npmjs.org/:_authToken']).toBe('fresh-token')
|
||||
} finally {
|
||||
if (originalXdg != null) {
|
||||
process.env.XDG_CONFIG_HOME = originalXdg
|
||||
} else {
|
||||
delete process.env.XDG_CONFIG_HOME
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('throw error if --save-prod is used with --save-peer', async () => {
|
||||
await expect(getConfig({
|
||||
cliOptions: {
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# @pnpm/run-npm
|
||||
|
||||
## 7.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 43cdd87: Node.js v16 support dropped. Use at least Node.js v18.12.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e70a829: The npm CLI executed from pnpm should not use Corepack [#7747](https://github.com/pnpm/pnpm/pull/7747).
|
||||
|
||||
## 6.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5a5e42551: Allow to set custom env.
|
||||
- 5a5e42551: Allow using token helpers in `pnpm publish` [#7316](https://github.com/pnpm/pnpm/issues/7316).
|
||||
|
||||
## 6.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- eceaa8b8b: Node.js 14 support dropped.
|
||||
|
||||
## 5.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- f884689e0: Require `@pnpm/logger` v5.
|
||||
|
||||
## 4.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9177ddbc9: If running npm errors, throw an error.
|
||||
|
||||
## 4.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 542014839: Node.js 12 is not supported.
|
||||
|
||||
## 3.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 724c5abd8: support "publishConfig.directory" field
|
||||
|
||||
## 3.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 97b986fbc: Node.js 10 support is dropped. At least Node.js 12.17 is required for the package to work.
|
||||
|
||||
## 2.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a2ef8084f: Use the same versions of dependencies across the pnpm monorepo.
|
||||
|
||||
## 2.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c56438567: Update cross-spawn to ^7.0.3
|
||||
|
||||
## 2.0.2
|
||||
@@ -1,15 +0,0 @@
|
||||
# @pnpm/run-npm
|
||||
|
||||
> Runs the npm CLI
|
||||
|
||||
[](https://www.npmjs.com/package/@pnpm/run-npm)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @pnpm/run-npm
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@pnpm/exec.run-npm",
|
||||
"version": "1000.0.0",
|
||||
"description": "Runs the npm CLI",
|
||||
"keywords": [
|
||||
"pnpm",
|
||||
"pnpm11"
|
||||
],
|
||||
"license": "MIT",
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"repository": "https://github.com/pnpm/pnpm/tree/main/exec/run-npm",
|
||||
"homepage": "https://github.com/pnpm/pnpm/tree/main/exec/run-npm#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pnpm/pnpm/issues"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"exports": {
|
||||
".": "./lib/index.js"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"!*.map"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"test": "pn compile",
|
||||
"prepublishOnly": "pn compile",
|
||||
"compile": "tsgo --build && pn lint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
"path-name": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/exec.run-npm": "workspace:*",
|
||||
"@types/cross-spawn": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.13"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "@pnpm/jest-config"
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import type childProcess from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
|
||||
import spawn from 'cross-spawn'
|
||||
import PATH from 'path-name'
|
||||
|
||||
export type NPMLocation = 'global' | 'user' | 'project'
|
||||
|
||||
export interface RunNPMOptions {
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
location?: NPMLocation
|
||||
userConfigPath?: string
|
||||
}
|
||||
|
||||
export function runNpm (npmPath: string | undefined, args: string[], options?: RunNPMOptions): childProcess.SpawnSyncReturns<Buffer> {
|
||||
const npm = npmPath ?? 'npm'
|
||||
return runScriptSync(npm, args, {
|
||||
cwd: options?.cwd ?? process.cwd(),
|
||||
stdio: 'inherit',
|
||||
userAgent: undefined,
|
||||
env: { ...options?.env, COREPACK_ENABLE_STRICT: '0' },
|
||||
location: options?.location,
|
||||
userConfigPath: options?.userConfigPath,
|
||||
})
|
||||
}
|
||||
|
||||
export function runScriptSync (
|
||||
command: string,
|
||||
args: string[],
|
||||
opts: {
|
||||
cwd: string
|
||||
location?: NPMLocation
|
||||
stdio: childProcess.StdioOptions
|
||||
userAgent?: string
|
||||
userConfigPath?: string
|
||||
env: Record<string, string>
|
||||
}
|
||||
): childProcess.SpawnSyncReturns<Buffer> {
|
||||
const env = {
|
||||
...createEnv(opts),
|
||||
...opts.env,
|
||||
}
|
||||
const result = spawn.sync(command, args, {
|
||||
...opts,
|
||||
env,
|
||||
})
|
||||
if (result.error) throw result.error
|
||||
return result
|
||||
}
|
||||
|
||||
function createEnv (
|
||||
opts: {
|
||||
cwd: string
|
||||
location?: NPMLocation
|
||||
userAgent?: string
|
||||
userConfigPath?: string
|
||||
}
|
||||
): NodeJS.ProcessEnv {
|
||||
const env = { ...process.env }
|
||||
|
||||
env[PATH] = [
|
||||
path.join(opts.cwd, 'node_modules', '.bin'),
|
||||
path.dirname(process.execPath),
|
||||
process.env[PATH],
|
||||
].join(path.delimiter)
|
||||
|
||||
if (opts.userAgent) {
|
||||
env.npm_config_user_agent = opts.userAgent
|
||||
}
|
||||
|
||||
if (opts.location) {
|
||||
env.npm_config_location = opts.location
|
||||
}
|
||||
|
||||
if (opts.userConfigPath) {
|
||||
env.npm_config_userconfig = opts.userConfigPath
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -2275,9 +2275,6 @@ importers:
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/error
|
||||
'@pnpm/exec.run-npm':
|
||||
specifier: workspace:*
|
||||
version: link:../../exec/run-npm
|
||||
'@pnpm/object.key-sorting':
|
||||
specifier: workspace:*
|
||||
version: link:../../object/key-sorting
|
||||
@@ -4128,22 +4125,6 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 7.0.1
|
||||
|
||||
exec/run-npm:
|
||||
dependencies:
|
||||
cross-spawn:
|
||||
specifier: 'catalog:'
|
||||
version: 7.0.6
|
||||
path-name:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.0
|
||||
devDependencies:
|
||||
'@pnpm/exec.run-npm':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
'@types/cross-spawn':
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.6
|
||||
|
||||
fetching/binary-fetcher:
|
||||
dependencies:
|
||||
'@pnpm/error':
|
||||
@@ -7231,9 +7212,6 @@ importers:
|
||||
'@pnpm/exec.commands':
|
||||
specifier: workspace:*
|
||||
version: link:../exec/commands
|
||||
'@pnpm/exec.run-npm':
|
||||
specifier: workspace:*
|
||||
version: link:../exec/run-npm
|
||||
'@pnpm/hooks.pnpmfile':
|
||||
specifier: workspace:*
|
||||
version: link:../hooks/pnpmfile
|
||||
|
||||
@@ -102,7 +102,6 @@
|
||||
"@pnpm/engine.runtime.commands": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/exec.commands": "workspace:*",
|
||||
"@pnpm/exec.run-npm": "workspace:*",
|
||||
"@pnpm/hooks.pnpmfile": "workspace:*",
|
||||
"@pnpm/installing.client": "workspace:*",
|
||||
"@pnpm/installing.commands": "workspace:*",
|
||||
|
||||
@@ -98,9 +98,6 @@
|
||||
{
|
||||
"path": "../exec/commands"
|
||||
},
|
||||
{
|
||||
"path": "../exec/run-npm"
|
||||
},
|
||||
{
|
||||
"path": "../hooks/pnpmfile"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user