Files
pnpm/config/reader/test/index.ts
Zoltan Kochan 2df8b71467 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.
2026-03-29 23:28:23 +02:00

1640 lines
42 KiB
TypeScript

/// <reference path="../../../__typings__/index.d.ts"/>
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { jest } from '@jest/globals'
import loadNpmConf from '@pnpm/npm-conf'
import { prepare, prepareEmpty } from '@pnpm/prepare'
import { fixtures } from '@pnpm/test-fixtures'
import PATH from 'path-name'
import { symlinkDir } from 'symlink-dir'
import { writeYamlFileSync } from 'write-yaml-file'
jest.unstable_mockModule('@pnpm/network.git-utils', () => ({ getCurrentBranch: jest.fn() }))
const { getConfig } = await import('@pnpm/config.reader')
const { getCurrentBranch } = await import('@pnpm/network.git-utils')
// To override any local settings,
// we force the default values of config
process.env['npm_config_hoist'] = 'true'
process.env['pnpm_config_hoist'] = 'true'
for (const suffix of [
'depth',
'registry',
'virtual_store_dir',
'shared_workspace_lockfile',
'node_version',
'fetch_retries',
]) {
delete process.env[`npm_config_${suffix}`]
delete process.env[`pnpm_config_${suffix}`]
}
const env = {
PNPM_HOME: import.meta.dirname,
[PATH]: path.join(import.meta.dirname, 'bin'),
}
const f = fixtures(import.meta.dirname)
test('getConfig()', async () => {
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config).toBeDefined()
expect(config.fetchRetries).toBe(2)
expect(config.fetchRetryFactor).toBe(10)
expect(config.fetchRetryMintimeout).toBe(10000)
expect(config.fetchRetryMaxtimeout).toBe(60000)
// nodeVersion should not have a default value.
// When not specified, the package-is-installable package detects nodeVersion automatically.
expect(config.nodeVersion).toBeUndefined()
})
test.each([
{ field: 'devEngines' as const, version: '22.20.0', onFail: 'download' as const, expected: '22.20.0' },
{ field: 'devEngines' as const, version: '22.20.0', onFail: 'error' as const, expected: '22.20.0' },
{ field: 'devEngines' as const, version: '^22.0.0', onFail: 'download' as const, expected: '22.0.0' },
{ field: 'engines' as const, version: '22.20.0', onFail: 'download' as const, expected: '22.20.0' },
])('when $field is $version and onFail is $onFail, nodeVersion is set to $expected', async ({ field, version, onFail, expected }) => {
prepare({
[field]: {
runtime: {
name: 'node',
version,
onFail,
},
},
})
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.nodeVersion).toBe(expected)
})
test('nodeVersion from config takes priority over devEngines.runtime', async () => {
prepare({
devEngines: {
runtime: {
name: 'node',
version: '22.20.0',
onFail: 'download',
},
},
})
const { config } = await getConfig({
cliOptions: {
'node-version': '20.0.0',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.nodeVersion).toBe('20.0.0')
})
test('throw error if --link-workspace-packages is used with --global', async () => {
await expect(getConfig({
cliOptions: {
global: true,
'link-workspace-packages': true,
},
env,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_CONFLICT_LINK_WORKSPACE_PACKAGES_WITH_GLOBAL',
message: 'Configuration conflict. "link-workspace-packages" may not be used with "global"',
})
})
test('correct settings on global install', async () => {
const { config } = await getConfig({
cliOptions: {
global: true,
save: false,
},
env,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.save).toBe(true)
})
test('throw error if --shared-workspace-lockfile is used with --global', async () => {
await expect(getConfig({
cliOptions: {
global: true,
'shared-workspace-lockfile': true,
},
env,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_CONFLICT_SHARED_WORKSPACE_LOCKFILE_WITH_GLOBAL',
message: 'Configuration conflict. "shared-workspace-lockfile" may not be used with "global"',
})
})
test('throw error if --lockfile-dir is used with --global', async () => {
await expect(getConfig({
cliOptions: {
global: true,
'lockfile-dir': '/home/src',
},
env,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_CONFLICT_LOCKFILE_DIR_WITH_GLOBAL',
message: 'Configuration conflict. "lockfile-dir" may not be used with "global"',
})
})
test('throw error if --hoist-pattern is used with --global', async () => {
await expect(getConfig({
cliOptions: {
global: true,
'hoist-pattern': 'eslint',
},
env,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_CONFLICT_HOIST_PATTERN_WITH_GLOBAL',
message: 'Configuration conflict. "hoist-pattern" may not be used with "global"',
})
})
test('throw error if --virtual-store-dir is used with --global', async () => {
await expect(getConfig({
cliOptions: {
global: true,
'virtual-store-dir': 'pkgs',
},
env,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_CONFLICT_VIRTUAL_STORE_DIR_WITH_GLOBAL',
message: 'Configuration conflict. "virtual-store-dir" may not be used with "global"',
})
})
test('.npmrc does not load pnpm settings', async () => {
prepareEmpty()
const npmrc = [
// npm options
'//my-org.registry.example.com:username=some-employee',
'//my-org.registry.example.com:_authToken=some-employee-token',
'@my-org:registry=https://my-org.registry.example.com',
'@jsr:registry=https://not-actually-jsr.example.com',
'username=example-user-name',
'_authToken=example-auth-token',
// pnpm options
'dlx-cache-max-age=1234',
'trust-policy-exclude[]=foo',
'trust-policy-exclude[]=bar',
'packages[]=baz',
'packages[]=qux',
].join('\n')
fs.writeFileSync('.npmrc', npmrc)
const { config } = await getConfig({
cliOptions: {
global: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
// rc options appear as usual
expect(config.rawConfig).toMatchObject({
'//my-org.registry.example.com:username': 'some-employee',
'//my-org.registry.example.com:_authToken': 'some-employee-token',
'@my-org:registry': 'https://my-org.registry.example.com',
'@jsr:registry': 'https://not-actually-jsr.example.com',
username: 'example-user-name',
_authToken: 'example-auth-token',
})
// workspace-specific settings are omitted
expect(config.rawConfig['dlx-cache-max-age']).toBeUndefined()
expect(config.rawConfig['dlxCacheMaxAge']).toBeUndefined()
expect(config.dlxCacheMaxAge).toBe(24 * 60) // TODO: refactor to make defaultOptions importable
expect(config.rawConfig['trust-policy-exclude']).toBeUndefined()
expect(config.rawConfig['trustPolicyExclude']).toBeUndefined()
expect(config.trustPolicyExclude).toBeUndefined()
expect(config.rawConfig.packages).toBeUndefined()
})
test('rc options appear as kebab-case in rawConfig even if it was defined as camelCase by pnpm-workspace.yaml', async () => {
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
ignoreScripts: true,
linkWorkspacePackages: true,
nodeLinker: 'hoisted',
sharedWorkspaceLockfile: true,
})
const { config } = await getConfig({
cliOptions: {
global: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config).toMatchObject({
ignoreScripts: true,
linkWorkspacePackages: true,
nodeLinker: 'hoisted',
sharedWorkspaceLockfile: true,
rawConfig: {
'ignore-scripts': true,
'link-workspace-packages': true,
'node-linker': 'hoisted',
'shared-workspace-lockfile': true,
},
})
expect(config.rawConfig.ignoreScripts).toBeUndefined()
expect(config.rawConfig.linkWorkspacePackages).toBeUndefined()
expect(config.rawConfig.nodeLinker).toBeUndefined()
expect(config.rawConfig.sharedWorkspaceLockfile).toBeUndefined()
})
test('workspace-specific settings preserve case in rawConfig', async () => {
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
packages: ['foo', 'bar'],
packageExtensions: {
'@babel/parser': {
peerDependencies: {
'@babel/types': '*',
},
},
'jest-circus': {
dependencies: {
slash: '3',
},
},
},
})
const { config } = await getConfig({
cliOptions: {
global: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.rawConfig.packages).toStrictEqual(['foo', 'bar'])
expect(config.rawConfig.packageExtensions).toStrictEqual({
'@babel/parser': {
peerDependencies: {
'@babel/types': '*',
},
},
'jest-circus': {
dependencies: {
slash: '3',
},
},
})
expect(config.rawConfig['package-extensions']).toBeUndefined()
expect(config.packageExtensions).toStrictEqual({
'@babel/parser': {
peerDependencies: {
'@babel/types': '*',
},
},
'jest-circus': {
dependencies: {
slash: '3',
},
},
})
})
test('when using --global, linkWorkspacePackages, sharedWorkspaceLockfile and lockfileDir are false even if they are set to true in pnpm-workspace.yaml', async () => {
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
linkWorkspacePackages: true,
sharedWorkspaceLockfile: true,
lockfileDir: true,
})
{
const { config } = await getConfig({
cliOptions: {
global: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.linkWorkspacePackages).toBeTruthy()
expect(config.sharedWorkspaceLockfile).toBeTruthy()
expect(config.lockfileDir).toBeTruthy()
}
{
const { config } = await getConfig({
cliOptions: {
global: true,
},
env,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.linkWorkspacePackages).toBeFalsy()
expect(config.sharedWorkspaceLockfile).toBeFalsy()
// FIXME: it supposed to return null but is undefined
expect(config.lockfileDir).toBeUndefined()
}
})
test('registries of scoped packages are read and normalized', async () => {
const { config } = await getConfig({
cliOptions: {
userconfig: path.join(import.meta.dirname, 'scoped-registries.ini'),
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.registries).toStrictEqual({
default: 'https://default.com/',
'@jsr': 'https://npm.jsr.io/',
'@foo': 'https://foo.com/',
'@bar': 'https://bar.com/',
'@qar': 'https://qar.com/qar',
})
})
test('registries in current directory\'s .npmrc have bigger priority then global config settings', async () => {
prepare()
fs.writeFileSync('.npmrc', 'registry=https://pnpm.io/', 'utf8')
const { config } = await getConfig({
cliOptions: {
userconfig: path.join(import.meta.dirname, 'scoped-registries.ini'),
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.registries).toStrictEqual({
default: 'https://pnpm.io/',
'@jsr': 'https://npm.jsr.io/',
'@foo': 'https://foo.com/',
'@bar': 'https://bar.com/',
'@qar': 'https://qar.com/qar',
})
})
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: {
'save-peer': true,
'save-prod': true,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_CONFLICT_PEER_CANNOT_BE_PROD_DEP',
message: 'A package cannot be a peer dependency and a prod dependency at the same time',
})
})
test('throw error if --save-optional is used with --save-peer', async () => {
await expect(getConfig({
cliOptions: {
'save-optional': true,
'save-peer': true,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_CONFLICT_PEER_CANNOT_BE_OPTIONAL_DEP',
message: 'A package cannot be a peer dependency and an optional dependency at the same time',
})
})
test('extraBinPaths', async () => {
prepareEmpty()
{
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
// extraBinPaths is empty outside of a workspace
expect(config.extraBinPaths).toHaveLength(0)
}
{
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
// extraBinPaths has the node_modules/.bin folder from the root of the workspace
expect(config.extraBinPaths).toStrictEqual([path.resolve('node_modules/.bin')])
}
{
const { config } = await getConfig({
cliOptions: {
'ignore-scripts': true,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
// extraBinPaths has the node_modules/.bin folder from the root of the workspace if scripts are ignored
expect(config.extraBinPaths).toStrictEqual([path.resolve('node_modules/.bin')])
}
{
const { config } = await getConfig({
cliOptions: {
'ignore-scripts': true,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
// extraBinPaths is empty inside a workspace if scripts are ignored
expect(config.extraBinPaths).toEqual([])
}
})
// hoist → hoistPattern processing is done in @pnpm/cli.utils
test('hoist-pattern is unchanged if --no-hoist used', async () => {
const { config } = await getConfig({
cliOptions: {
hoist: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.hoist).toBe(false)
expect(config.hoistPattern).toStrictEqual(['*'])
})
test('throw error if --no-hoist is used with --shamefully-hoist', async () => {
await expect(getConfig({
cliOptions: {
hoist: false,
'shamefully-hoist': true,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_CONFLICT_HOIST',
message: '--shamefully-hoist cannot be used with --no-hoist',
})
})
test('throw error if --no-hoist is used with --hoist-pattern', async () => {
await expect(getConfig({
cliOptions: {
hoist: false,
'hoist-pattern': 'eslint-*',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})).rejects.toMatchObject({
code: 'ERR_PNPM_CONFIG_CONFLICT_HOIST',
message: '--hoist-pattern cannot be used with --no-hoist',
})
})
// public-hoist-pattern normalization is done in @pnpm/cli.utils
test('normalizing the value of public-hoist-pattern', async () => {
{
const { config } = await getConfig({
cliOptions: {
'public-hoist-pattern': '',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.publicHoistPattern).toBe('')
}
{
const { config } = await getConfig({
cliOptions: {
'public-hoist-pattern': [''],
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.publicHoistPattern).toStrictEqual([''])
}
})
test.skip('rawLocalConfig in a workspace', async () => {
prepareEmpty()
const workspaceDir = process.cwd()
fs.writeFileSync('.npmrc', 'hoist-pattern=*', 'utf8')
fs.mkdirSync('package')
process.chdir('package')
fs.writeFileSync('.npmrc', 'hoist-pattern=eslint-*', 'utf8')
{
const { config } = await getConfig({
cliOptions: {
'save-exact': true,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir,
})
expect(config.rawLocalConfig).toStrictEqual({
'hoist-pattern': 'eslint-*',
'save-exact': true,
})
}
// package w/o its own .npmrc
fs.mkdirSync('package2')
process.chdir('package2')
{
const { config } = await getConfig({
cliOptions: {
'save-exact': true,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir,
})
expect(config.rawLocalConfig).toStrictEqual({
'hoist-pattern': '*',
'save-exact': true,
})
}
})
test.skip('rawLocalConfig', async () => {
prepareEmpty()
fs.writeFileSync('.npmrc', 'modules-dir=modules', 'utf8')
const { config } = await getConfig({
cliOptions: {
'save-exact': true,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.rawLocalConfig).toStrictEqual({
'modules-dir': 'modules',
'save-exact': true,
})
})
test('normalize the value of the color flag', async () => {
{
const { config } = await getConfig({
cliOptions: {
color: true,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.color).toBe('always')
}
{
const { config } = await getConfig({
cliOptions: {
color: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.color).toBe('never')
}
})
// NOTE: This test currently fails as pnpm currently lack a way to verify pnpm-workspace.yaml
test.skip('read only supported settings from config', async () => {
prepare()
writeYamlFileSync('pnpm-workspace.yaml', {
storeDir: '__store__',
foo: 'bar',
})
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.storeDir).toBe('__store__')
// @ts-expect-error
expect(config['foo']).toBeUndefined() // NOTE: This line current fails as there are yet a way to verify fields in pnpm-workspace.yaml
expect(config.rawConfig['foo']).toBe('bar')
})
test('all CLI options are added to the config', async () => {
const { config } = await getConfig({
cliOptions: {
'foo-bar': 'qar',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
// @ts-expect-error
expect(config['fooBar']).toBe('qar')
})
test('local prefix search stops on pnpm-workspace.yaml', async () => {
const workspaceDir = path.join(import.meta.dirname, 'has-workspace-yaml')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.dir).toEqual(workspaceDir)
})
test('reads workspacePackagePatterns', async () => {
const workspaceDir = path.join(import.meta.dirname, 'fixtures/pkg-with-valid-workspace-yaml')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir,
})
expect(config.workspacePackagePatterns).toEqual(['packages/*'])
})
test('workspacePackagePatterns defaults to ["."] when pnpm-workspace.yaml has no packages field', async () => {
const workspaceDir = path.join(import.meta.dirname, 'fixtures/workspace-yaml-without-packages')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir,
})
expect(config.workspacePackagePatterns).toEqual(['.'])
})
test('setting workspace-concurrency to negative number', async () => {
const workspaceDir = path.join(import.meta.dirname, 'fixtures/pkg-with-valid-workspace-yaml')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {
'workspace-concurrency': -1,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir,
})
expect(config.workspaceConcurrency >= 1).toBeTruthy()
})
test('respects testPattern', async () => {
{
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.testPattern).toBeUndefined()
}
{
const workspaceDir = path.join(import.meta.dirname, 'using-test-pattern')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir,
})
expect(config.testPattern).toEqual(['*.spec.js', '*.spec.ts'])
}
{
const workspaceDir = path.join(import.meta.dirname, 'ignore-test-pattern')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir,
})
expect(config.testPattern).toBeUndefined()
}
})
test('respects changedFilesIgnorePattern', async () => {
{
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.changedFilesIgnorePattern).toBeUndefined()
}
{
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
changedFilesIgnorePattern: ['.github/**', '**/README.md'],
})
const { config } = await getConfig({
cliOptions: {
global: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.changedFilesIgnorePattern).toEqual(['.github/**', '**/README.md'])
}
})
test('dir is resolved to real path', async () => {
prepareEmpty()
const realDir = path.resolve('real-path')
fs.mkdirSync(realDir)
const symlink = path.resolve('symlink')
await symlinkDir(realDir, symlink)
const { config } = await getConfig({
cliOptions: { dir: symlink },
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.dir).toBe(realDir)
})
test('warn user unknown settings in npmrc', async () => {
prepare()
const npmrc = [
'typo-setting=true',
' ',
'mistake-setting=false',
'//foo.bar:_authToken=aaa',
'@qar:registry=https://registry.example.org/',
].join('\n')
fs.writeFileSync('.npmrc', npmrc, 'utf8')
const { warnings } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
checkUnknownSetting: true,
})
expect(warnings).toStrictEqual([
'Your .npmrc file contains unknown setting: typo-setting, mistake-setting',
])
const { warnings: noWarnings } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(noWarnings).toStrictEqual([])
})
test('getConfig() converts noproxy to noProxy', async () => {
const { config } = await getConfig({
cliOptions: {
noproxy: 'www.foo.com',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.noProxy).toBe('www.foo.com')
})
test('getConfig() returns the userconfig', async () => {
prepareEmpty()
fs.mkdirSync('user-home')
fs.writeFileSync(path.resolve('user-home', '.npmrc'), 'registry = https://registry.example.test', 'utf-8')
loadNpmConf.defaults.userconfig = path.resolve('user-home', '.npmrc')
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.userConfig).toEqual({ registry: 'https://registry.example.test' })
})
test('getConfig() returns the userconfig even when overridden locally', async () => {
prepareEmpty()
fs.mkdirSync('user-home')
fs.writeFileSync(path.resolve('user-home', '.npmrc'), 'registry = https://registry.example.test', 'utf-8')
loadNpmConf.defaults.userconfig = path.resolve('user-home', '.npmrc')
fs.writeFileSync('.npmrc', 'registry = https://project-local.example.test', 'utf-8')
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.registry).toBe('https://project-local.example.test')
expect(config.userConfig).toEqual({ registry: 'https://registry.example.test' })
})
test('getConfig() sets sideEffectsCacheRead and sideEffectsCacheWrite when side-effects-cache is set', async () => {
const { config } = await getConfig({
cliOptions: {
'side-effects-cache': true,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config).toBeDefined()
expect(config.sideEffectsCacheRead).toBeTruthy()
expect(config.sideEffectsCacheWrite).toBeTruthy()
})
test('getConfig() should read cafile', async () => {
const { config } = await getConfig({
cliOptions: {
cafile: path.join(import.meta.dirname, 'cafile.txt'),
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config).toBeDefined()
expect(config.ca).toStrictEqual([`xxx
-----END CERTIFICATE-----`])
})
test('getConfig() should read inline SSL certificates from .npmrc', async () => {
prepareEmpty()
// These are written to .npmrc with literal \n strings
const inlineCa = '-----BEGIN CERTIFICATE-----\\nMIIFNzCCAx+gAwIBAgIQNB613yRzpKtDztlXiHmOGDANBgkqhkiG9w0BAQsFADAR\\n-----END CERTIFICATE-----'
const inlineCert = '-----BEGIN CERTIFICATE-----\\nMIIClientCert\\n-----END CERTIFICATE-----'
const inlineKey = '-----BEGIN PRIVATE KEY-----\\nMIIClientKey\\n-----END PRIVATE KEY-----'
const npmrc = [
'//registry.example.com/:ca=' + inlineCa,
'//registry.example.com/:cert=' + inlineCert,
'//registry.example.com/:key=' + inlineKey,
].join('\n')
fs.writeFileSync('.npmrc', npmrc, 'utf8')
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
// After processing, \n should be converted to actual newlines
expect(config.sslConfigs).toBeDefined()
expect(config.sslConfigs['//registry.example.com/']).toStrictEqual({
ca: inlineCa.replace(/\\n/g, '\n'),
cert: inlineCert.replace(/\\n/g, '\n'),
key: inlineKey.replace(/\\n/g, '\n'),
})
})
test('respect mergeGitBranchLockfilesBranchPattern', async () => {
{
prepareEmpty()
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.mergeGitBranchLockfilesBranchPattern).toBeUndefined()
expect(config.mergeGitBranchLockfiles).toBeUndefined()
}
{
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
mergeGitBranchLockfilesBranchPattern: ['main', 'release/**'],
})
const { config } = await getConfig({
cliOptions: {
global: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.mergeGitBranchLockfilesBranchPattern).toEqual(['main', 'release/**'])
}
})
test('getConfig() sets mergeGitBranchLockfiles when branch matches mergeGitBranchLockfilesBranchPattern', async () => {
prepareEmpty()
{
writeYamlFileSync('pnpm-workspace.yaml', {
mergeGitBranchLockfilesBranchPattern: ['main', 'release/**'],
})
jest.mocked(getCurrentBranch).mockReturnValue(Promise.resolve('develop'))
const { config } = await getConfig({
cliOptions: {
global: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.mergeGitBranchLockfilesBranchPattern).toEqual(['main', 'release/**'])
expect(config.mergeGitBranchLockfiles).toBe(false)
}
{
jest.mocked(getCurrentBranch).mockReturnValue(Promise.resolve('main'))
const { config } = await getConfig({
cliOptions: {
global: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.mergeGitBranchLockfiles).toBe(true)
}
{
jest.mocked(getCurrentBranch).mockReturnValue(Promise.resolve('release/1.0.0'))
const { config } = await getConfig({
cliOptions: {
global: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.mergeGitBranchLockfiles).toBe(true)
}
})
test('preferSymlinkedExecutables should be true when nodeLinker is hoisted', async () => {
prepareEmpty()
const { config } = await getConfig({
cliOptions: {
'node-linker': 'hoisted',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.preferSymlinkedExecutables).toBeTruthy()
})
test('return a warning when the .npmrc has an env variable that does not exist', async () => {
fs.writeFileSync('.npmrc', 'registry=${ENV_VAR_123}', 'utf8')
const { warnings } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
const expected = [
expect.stringContaining('Failed to replace env in config: ${ENV_VAR_123}') // eslint-disable-line
]
expect(warnings).toEqual(expect.arrayContaining(expected))
})
test('getConfig() returns failedToLoadBuiltInConfig', async () => {
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.failedToLoadBuiltInConfig).toBeDefined()
})
test('return a warning if a package.json has workspaces field but there is no pnpm-workspaces.yaml file', async () => {
const prefix = f.find('pkg-using-workspaces')
const { warnings } = await getConfig({
cliOptions: { dir: prefix },
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(warnings).toStrictEqual([
'The "workspaces" field in package.json is not supported by pnpm. Create a "pnpm-workspace.yaml" file instead.',
])
})
test('do not return a warning if a package.json has workspaces field and there is a pnpm-workspace.yaml file', async () => {
const prefix = f.find('pkg-using-workspaces')
const { warnings } = await getConfig({
cliOptions: { dir: prefix },
workspaceDir: prefix,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(warnings).toStrictEqual([])
})
test('read PNPM_HOME defined in environment variables', async () => {
const oldEnv = process.env
const homeDir = './specified-dir'
process.env = {
...oldEnv,
PNPM_HOME: homeDir,
}
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.pnpmHomeDir).toBe(homeDir)
process.env = oldEnv
})
test('xxx', async () => {
const oldEnv = process.env
process.env = {
...oldEnv,
FOO: 'registry',
}
const { config } = await getConfig({
cliOptions: {
dir: f.find('has-env-in-key'),
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.registry).toBe('https://registry.example.com/')
process.env = oldEnv
})
test('settings from pnpm-workspace.yaml are read', async () => {
const workspaceDir = f.find('settings-in-workspace-yaml')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {},
workspaceDir,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.trustPolicyExclude).toStrictEqual(['foo', 'bar'])
expect(config.rawConfig['trust-policy-exclude']).toStrictEqual(['foo', 'bar'])
})
test('settings sharedWorkspaceLockfile in pnpm-workspace.yaml should take effect', async () => {
const workspaceDir = f.find('settings-in-workspace-yaml')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {},
workspaceDir,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.sharedWorkspaceLockfile).toBe(false)
expect(config.lockfileDir).toBeUndefined()
})
// shamefullyHoist → publicHoistPattern conversion is done in @pnpm/cli.utils
test('settings shamefullyHoist in pnpm-workspace.yaml should take effect', async () => {
const workspaceDir = f.find('settings-in-workspace-yaml')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {},
workspaceDir,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.shamefullyHoist).toBe(true)
expect(config.rawConfig['shamefully-hoist']).toBe(true)
})
test('settings gitBranchLockfile in pnpm-workspace.yaml should take effect', async () => {
const workspaceDir = f.find('settings-in-workspace-yaml')
process.chdir(workspaceDir)
const { config } = await getConfig({
cliOptions: {},
workspaceDir,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.gitBranchLockfile).toBe(true)
expect(config.useGitBranchLockfile).toBe(true)
expect(config.rawConfig['git-branch-lockfile']).toBe(true)
})
test('loads setting from environment variable pnpm_config_*', async () => {
prepareEmpty()
const { config } = await getConfig({
cliOptions: {},
env: {
pnpm_config_fetch_retries: '100',
pnpm_config_hoist_pattern: '["react", "react-dom"]',
pnpm_config_use_node_version: '22.0.0',
pnpm_config_trust_policy_exclude: '["foo", "bar"]',
pnpm_config_registry: 'https://registry.example.com',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.fetchRetries).toBe(100)
expect(config.hoistPattern).toStrictEqual(['react', 'react-dom'])
expect(config.trustPolicyExclude).toStrictEqual(['foo', 'bar'])
expect(config.registry).toBe('https://registry.example.com/')
expect(config.registries.default).toBe('https://registry.example.com/')
})
test('environment variable pnpm_config_* should override pnpm-workspace.yaml', async () => {
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
fetchRetries: 5,
})
async function getConfigValue (env: NodeJS.ProcessEnv): Promise<number | undefined> {
const { config } = await getConfig({
cliOptions: {},
env,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
return config.fetchRetries
}
expect(await getConfigValue({})).toBe(5)
expect(await getConfigValue({
pnpm_config_fetch_retries: '10',
})).toBe(10)
})
test('CLI should override environment variable pnpm_config_*', async () => {
prepareEmpty()
async function getConfigValue (cliOptions: Record<string, unknown>): Promise<number | undefined> {
const { config } = await getConfig({
cliOptions,
env: {
pnpm_config_fetch_retries: '5',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
return config.fetchRetries
}
expect(await getConfigValue({})).toBe(5)
expect(await getConfigValue({
fetchRetries: 10,
})).toBe(10)
expect(await getConfigValue({
'fetch-retries': 10,
})).toBe(10)
})
test('warn when directory contains PATH delimiter character', async () => {
const tempDir = path.join(os.tmpdir(), `pnpm-test${path.delimiter}project-${Date.now()}`)
fs.mkdirSync(tempDir, { recursive: true })
try {
const { warnings } = await getConfig({
cliOptions: { dir: tempDir },
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(warnings).toContainEqual(
expect.stringContaining('path delimiter character')
)
} finally {
fs.rmSync(tempDir, { recursive: true })
}
})
test('no warning when directory does not contain PATH delimiter character', async () => {
const tempDir = path.join(os.tmpdir(), `pnpm-test-normal-${Date.now()}`)
fs.mkdirSync(tempDir, { recursive: true })
try {
const { warnings } = await getConfig({
cliOptions: { dir: tempDir },
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(warnings).not.toContainEqual(
expect.stringContaining('path delimiter character')
)
} finally {
fs.rmSync(tempDir, { recursive: true })
}
})
test.each([
[undefined, undefined],
[false, undefined],
[true, true],
])('sets autoConfirmAllPrompts when CLI is passed --yes=%s', async (cliValue?: boolean, expectedValue?: boolean) => {
const { config } = await getConfig({
cliOptions: {
'yes': cliValue,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.autoConfirmAllPrompts).toBe(expectedValue)
})
describe('global config.yaml', () => {
let XDG_CONFIG_HOME: string | undefined
beforeEach(() => {
XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME
})
afterEach(() => {
process.env.XDG_CONFIG_HOME = XDG_CONFIG_HOME
})
test('reads config from global config.yaml', async () => {
prepareEmpty()
fs.mkdirSync('.config/pnpm', { recursive: true })
writeYamlFileSync('.config/pnpm/config.yaml', {
dangerouslyAllowAllBuilds: true,
})
// TODO: `getConfigDir`, `getHomeDir`, etc. (from dirs.ts) should allow customizing env or process.
// TODO: after that, remove this `describe` wrapper.
process.env.XDG_CONFIG_HOME = path.resolve('.config')
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.dangerouslyAllowAllBuilds).toBe(true)
// NOTE: the field may appear kebab-case here, but only internally,
// `pnpm config list` would convert them to camelCase.
// TODO: switch to camelCase entirely later.
expect(config.rawConfig).toHaveProperty(['dangerously-allow-all-builds'])
})
})
test('lockfile: false in pnpm-workspace.yaml sets useLockfile to false', async () => {
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
lockfile: false,
})
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.useLockfile).toBe(false)
})
test('pnpm_config_lockfile env var overrides lockfile from pnpm-workspace.yaml in useLockfile', async () => {
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
lockfile: true,
})
const { config } = await getConfig({
cliOptions: {},
env: {
pnpm_config_lockfile: 'false',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.useLockfile).toBe(false)
})
test('ci disables enableGlobalVirtualStore by default', async () => {
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
ci: true,
})
const { config } = await getConfig({
cliOptions: {},
env,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.enableGlobalVirtualStore).toBe(false)
})
test('ci respects explicit enableGlobalVirtualStore from config', async () => {
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
ci: true,
enableGlobalVirtualStore: true,
})
const { config } = await getConfig({
cliOptions: {},
env,
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.enableGlobalVirtualStore).toBe(true)
})
test('pnpm_config_git_branch_lockfile env var overrides git-branch-lockfile from pnpm-workspace.yaml in useGitBranchLockfile', async () => {
prepareEmpty()
writeYamlFileSync('pnpm-workspace.yaml', {
gitBranchLockfile: false,
})
const { config } = await getConfig({
cliOptions: {},
env: {
pnpm_config_git_branch_lockfile: 'true',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.useGitBranchLockfile).toBe(true)
})