mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat(config): global yaml (#10145)
* feat(config): global `rc.yaml`
* fix: undefined `rawConfig`
* test: add a test
* feat: re-export `isSupportedNpmConfig`
* feat: return `'compat'` to distinguish compatibility reason
* docs: `isSupportedNpmConfig`
* fix: eslint
* docs: clarify the case of the config key
* feat(cli/config/set): target yaml for pnpm-specific settings
* fix: read the correct file
* fix: write to the correct directory
* refactor: remove disabled code
* refactor: get `configDir` directly
* docs: remove outdated documentation
* test: fix a test
* test: rename
* fix: explicitly tell npm the config file path
* test: add a test
* test: add a test
* test: fix a test
* fix: local config dir
* fix: `managingAuthSettings`
* test: rename
* test: fix
* test: add a test
* test: demonstrate choosing config files
* test: fix
* docs: yet another consideration
* test: demonstrate choosing config files
* fix: correct local config file names in test helper
* test: demonstrate choosing config files
* test: use the helper
* test: add a test
* test: correct a test
* test: fix
* test: fix
* fix: eslint
* test: remove duplicate
* feat: validate `rc.yaml`
* docs: changeset
* test: fix `configDelete.test.ts`
* feat: other `npm` call-sites
* fix: make optional again
* feat: remove the change from `publish`
* fix: eslint
* refactor: just one is sufficient
* refactor: replace type union with 3 functions
* refactor(test): extract helper functions
* fix: add `rc.yaml` to `rawConfig`
* test: keep workspace settings out of `rc.yaml`
* test: fix `spawn ENOENT`
* chore(git): revert invalid change
This reverts commit 1ff6fe2323.
* feat: rename `rc.yaml` to `config.yaml`
* refactor: replace `acceptNonRc` with `!globalSettingsOnly`
* feat!: remove compat completely
* refactor: rename a function
* fix: no actual catalogs
* refactor: replace bool flag with preemptive filter
* feat!: filter global config keys
* test: fix
* fix: exclude `deploy-all-files`
* fix: reverse schema merge order
* feat(cli/config/set): validate global config yaml key
* test: remove duplicated assertion
* docs: correct
* docs: goal changed
This commit is contained in:
15
.changeset/great-trams-drop.md
Normal file
15
.changeset/great-trams-drop.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-config": minor
|
||||
"@pnpm/workspace.manifest-writer": minor
|
||||
"@pnpm/workspace.read-manifest": minor
|
||||
"@pnpm/constants": minor
|
||||
"@pnpm/config": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Add support for a global YAML config file named `config.yaml`.
|
||||
|
||||
Now configurations are divided into 2 categories:
|
||||
|
||||
- Registry and auth settings which can be stored in INI files such as global `rc` and local `.npmrc`.
|
||||
- pnpm-specific settings which can only be loaded from YAML files such as global `config.yaml` and local `pnpm-workspace.yaml`.
|
||||
@@ -40,16 +40,8 @@ const AUTH_CFG_KEYS = [
|
||||
'strictSsl',
|
||||
] satisfies Array<keyof Config>
|
||||
|
||||
const PNPM_COMPAT_SETTINGS = [
|
||||
// NOTE: This field is kept in .npmrc because `managePackageManagerVersions: true`
|
||||
// in pnpm-workspace.yaml currently causes pnpm to be unresponsive (probably
|
||||
// due to an infinite loop of some kind).
|
||||
'manage-package-manager-versions',
|
||||
] satisfies Array<keyof typeof types>
|
||||
|
||||
const NPM_AUTH_SETTINGS = [
|
||||
...RAW_AUTH_CFG_KEYS,
|
||||
...PNPM_COMPAT_SETTINGS,
|
||||
'_auth',
|
||||
'_authToken',
|
||||
'_password',
|
||||
@@ -92,14 +84,20 @@ export function inheritAuthConfig (targetCfg: InheritableConfig, authSrcCfg: Inh
|
||||
inheritPickedConfig(targetCfg, authSrcCfg, pickAuthConfig, pickRawAuthConfig)
|
||||
}
|
||||
|
||||
export const isSupportedNpmConfig = (key: string): boolean =>
|
||||
/**
|
||||
* Whether the config key would be read from an INI config file.
|
||||
*/
|
||||
export const isIniConfigKey = (key: string): boolean =>
|
||||
key.startsWith('@') || key.startsWith('//') || NPM_AUTH_SETTINGS.includes(key)
|
||||
|
||||
export function pickNpmAuthConfig<RawConfig extends Record<string, unknown>> (rawConfig: RawConfig): Partial<RawConfig> {
|
||||
/**
|
||||
* Filter keys that are allowed to be read from an INI config file.
|
||||
*/
|
||||
export function pickIniConfig<RawConfig extends Record<string, unknown>> (rawConfig: RawConfig): Partial<RawConfig> {
|
||||
const result: Partial<RawConfig> = {}
|
||||
|
||||
for (const key in rawConfig) {
|
||||
if (isSupportedNpmConfig(key)) {
|
||||
if (isIniConfigKey(key)) {
|
||||
result[key] = rawConfig[key]
|
||||
}
|
||||
}
|
||||
|
||||
183
config/config/src/configFileKey.ts
Normal file
183
config/config/src/configFileKey.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import npmTypes from '@pnpm/npm-conf/lib/types.js'
|
||||
import { type pnpmTypes } from './types.js'
|
||||
|
||||
type NpmKey = keyof typeof npmTypes.types
|
||||
type PnpmKey = keyof typeof pnpmTypes
|
||||
|
||||
/**
|
||||
* Keys from {@link pnpmTypes} that are valid fields in a global config file.
|
||||
*/
|
||||
export const pnpmConfigFileKeys = [
|
||||
'bail',
|
||||
'ci',
|
||||
'color',
|
||||
'cache-dir',
|
||||
'child-concurrency',
|
||||
'dangerously-allow-all-builds',
|
||||
'enable-modules-dir',
|
||||
'enable-global-virtual-store',
|
||||
'exclude-links-from-lockfile',
|
||||
'extend-node-path',
|
||||
'fetch-timeout',
|
||||
'fetch-warn-timeout-ms',
|
||||
'fetch-min-speed-ki-bps',
|
||||
'fetching-concurrency',
|
||||
'git-checks',
|
||||
'git-shallow-hosts',
|
||||
'global-bin-dir',
|
||||
'global-dir',
|
||||
'global-path',
|
||||
'global-pnpmfile',
|
||||
'optimistic-repeat-install',
|
||||
'loglevel',
|
||||
'maxsockets',
|
||||
'modules-cache-max-age',
|
||||
'dlx-cache-max-age',
|
||||
'minimum-release-age',
|
||||
'minimum-release-age-exclude',
|
||||
'network-concurrency',
|
||||
'noproxy',
|
||||
'npm-path',
|
||||
'package-import-method',
|
||||
'prefer-frozen-lockfile',
|
||||
'prefer-offline',
|
||||
'prefer-symlinked-executables',
|
||||
'reporter',
|
||||
'resolution-mode',
|
||||
'store-dir',
|
||||
'use-beta-cli',
|
||||
'use-running-store-server',
|
||||
'use-store-server',
|
||||
] as const satisfies readonly PnpmKey[]
|
||||
export type PnpmConfigFileKey = typeof pnpmConfigFileKeys[number]
|
||||
|
||||
/**
|
||||
* Keys that present in {@link pnpmTypes} but are excluded from {@link ConfigFileKey}.
|
||||
* They are usually CLI flags or workspace-only settings.
|
||||
*/
|
||||
export const excludedPnpmKeys = [
|
||||
'auto-install-peers',
|
||||
'catalog-mode',
|
||||
'config-dir',
|
||||
'merge-git-branch-lockfiles',
|
||||
'merge-git-branch-lockfiles-branch-pattern',
|
||||
'deploy-all-files',
|
||||
'dedupe-peer-dependents',
|
||||
'dedupe-direct-deps',
|
||||
'dedupe-injected-deps',
|
||||
'dev',
|
||||
'dir',
|
||||
'disallow-workspace-cycles',
|
||||
'enable-pre-post-scripts',
|
||||
'filter',
|
||||
'filter-prod',
|
||||
'force-legacy-deploy',
|
||||
'frozen-lockfile',
|
||||
'git-branch-lockfile',
|
||||
'hoist',
|
||||
'hoist-pattern',
|
||||
'hoist-workspace-packages',
|
||||
'ignore-compatibility-db',
|
||||
'ignore-dep-scripts',
|
||||
'ignore-pnpmfile',
|
||||
'ignore-workspace',
|
||||
'ignore-workspace-cycles',
|
||||
'ignore-workspace-root-check',
|
||||
'include-workspace-root',
|
||||
'init-package-manager',
|
||||
'init-type',
|
||||
'inject-workspace-packages',
|
||||
'legacy-dir-filtering',
|
||||
'link-workspace-packages',
|
||||
'lockfile',
|
||||
'lockfile-dir',
|
||||
'lockfile-directory',
|
||||
'lockfile-include-tarball-url',
|
||||
'lockfile-only',
|
||||
'manage-package-manager-versions',
|
||||
'modules-dir',
|
||||
'node-linker',
|
||||
'offline',
|
||||
'only-built-dependencies',
|
||||
'pack-destination',
|
||||
'pack-gzip-level',
|
||||
'patches-dir',
|
||||
'pnpmfile',
|
||||
'package-manager-strict',
|
||||
'package-manager-strict-version',
|
||||
'prefer-workspace-packages',
|
||||
'preserve-absolute-paths',
|
||||
'production',
|
||||
'public-hoist-pattern',
|
||||
'publish-branch',
|
||||
'recursive-install',
|
||||
'resolve-peers-from-workspace-root',
|
||||
'aggregate-output',
|
||||
'reporter-hide-prefix',
|
||||
'save-catalog-name',
|
||||
'save-peer',
|
||||
'save-workspace-protocol',
|
||||
'script-shell',
|
||||
'shamefully-flatten',
|
||||
'shamefully-hoist',
|
||||
'shared-workspace-lockfile',
|
||||
'shell-emulator',
|
||||
'side-effects-cache',
|
||||
'side-effects-cache-readonly',
|
||||
'symlink',
|
||||
'sort',
|
||||
'state-dir',
|
||||
'stream',
|
||||
'strict-dep-builds',
|
||||
'strict-store-pkg-content-check',
|
||||
'strict-peer-dependencies',
|
||||
'trust-policy',
|
||||
'use-node-version',
|
||||
'use-stderr',
|
||||
'verify-deps-before-run',
|
||||
'verify-store-integrity',
|
||||
'global-virtual-store-dir',
|
||||
'virtual-store-dir',
|
||||
'virtual-store-dir-max-length',
|
||||
'peers-suffix-max-length',
|
||||
'workspace-concurrency',
|
||||
'workspace-packages',
|
||||
'workspace-root',
|
||||
'test-pattern',
|
||||
'changed-files-ignore-pattern',
|
||||
'embed-readme',
|
||||
'update-notifier',
|
||||
'registry-supports-time-field',
|
||||
'fail-if-no-match',
|
||||
'sync-injected-deps-after-scripts',
|
||||
'cpu',
|
||||
'libc',
|
||||
'os',
|
||||
] as const satisfies ReadonlyArray<Exclude<PnpmKey, PnpmConfigFileKey>>
|
||||
export type ExcludedPnpmKey = typeof excludedPnpmKeys[number]
|
||||
|
||||
/**
|
||||
* Proof that {@link excludedPnpmKeys} is complete and exhaustive, i.e. All keys that appear in {@link pnpmTypes} but not in
|
||||
* {@link pnpmConfigFileKeys} should be included in {@link excludedPnpmKeys}.
|
||||
*/
|
||||
export const _proofExcludedPnpmKeysIsExhaustive = (carrier: Exclude<PnpmKey, PnpmConfigFileKey>): ExcludedPnpmKey => carrier
|
||||
|
||||
/**
|
||||
* Proof that there are no keys that are both included and excluded, i.e. {@link pnpmConfigFileKeys} and {@link excludedPnpmKeys}
|
||||
* have no overlap.
|
||||
*/
|
||||
export const _proofNoContradiction = (carrier: PnpmConfigFileKey & ExcludedPnpmKey): never => carrier
|
||||
|
||||
// even npmTypes still have keys that don't make sense in global config, but the list is quite long, let's do it another day.
|
||||
// TODO: compile a list of npm keys that are valid or invalid in a global config file.
|
||||
export type NpmConfigFileKey = Exclude<NpmKey, ExcludedPnpmKey>
|
||||
|
||||
/** Key that is valid in a global config file. */
|
||||
export type ConfigFileKey = NpmConfigFileKey | PnpmConfigFileKey
|
||||
|
||||
const setOfPnpmConfigFilesKeys: ReadonlySet<string> = new Set(pnpmConfigFileKeys)
|
||||
const setOfExcludedPnpmKeys: ReadonlySet<string> = new Set(excludedPnpmKeys)
|
||||
|
||||
/** Whether the key (in kebab-case) is a valid key in a global config file. */
|
||||
export const isConfigFileKey = (kebabKey: string): kebabKey is ConfigFileKey =>
|
||||
setOfPnpmConfigFilesKeys.has(kebabKey) || (kebabKey in npmTypes.types && !setOfExcludedPnpmKeys.has(kebabKey))
|
||||
@@ -64,7 +64,7 @@ export function getOptionsFromRootManifest (manifestDir: string, manifest: Proje
|
||||
return settings
|
||||
}
|
||||
|
||||
export function getOptionsFromPnpmSettings (manifestDir: string, pnpmSettings: PnpmSettings, manifest?: ProjectManifest): OptionsFromRootManifest {
|
||||
export function getOptionsFromPnpmSettings (manifestDir: string | undefined, pnpmSettings: PnpmSettings, manifest?: ProjectManifest): OptionsFromRootManifest {
|
||||
const renamedKeys = ['allowNonAppliedPatches'] as const satisfies Array<keyof PnpmSettings>
|
||||
const settings: OptionsFromRootManifest = omit(renamedKeys, replaceEnvInSettings(pnpmSettings))
|
||||
if (settings.overrides) {
|
||||
@@ -74,13 +74,13 @@ export function getOptionsFromPnpmSettings (manifestDir: string, pnpmSettings: P
|
||||
settings.overrides = mapValues(createVersionReferencesReplacer(manifest), settings.overrides)
|
||||
}
|
||||
}
|
||||
if (pnpmSettings.onlyBuiltDependenciesFile) {
|
||||
if (pnpmSettings.onlyBuiltDependenciesFile && manifestDir != null) {
|
||||
settings.onlyBuiltDependenciesFile = path.join(manifestDir, pnpmSettings.onlyBuiltDependenciesFile)
|
||||
}
|
||||
if (pnpmSettings.patchedDependencies) {
|
||||
settings.patchedDependencies = { ...pnpmSettings.patchedDependencies }
|
||||
for (const [dep, patchFile] of Object.entries(pnpmSettings.patchedDependencies)) {
|
||||
if (path.isAbsolute(patchFile)) continue
|
||||
if (manifestDir == null || path.isAbsolute(patchFile)) continue
|
||||
settings.patchedDependencies[dep] = path.join(manifestDir, patchFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import os from 'os'
|
||||
import { isCI } from 'ci-info'
|
||||
import { omit } from 'ramda'
|
||||
import { getCatalogsFromWorkspaceManifest } from '@pnpm/catalogs.config'
|
||||
import { LAYOUT_VERSION } from '@pnpm/constants'
|
||||
import { GLOBAL_CONFIG_YAML_FILENAME, LAYOUT_VERSION } from '@pnpm/constants'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { isCamelCase } from '@pnpm/naming-cases'
|
||||
import loadNpmConf from '@pnpm/npm-conf'
|
||||
@@ -12,6 +12,7 @@ import type npmTypes from '@pnpm/npm-conf/lib/types.js'
|
||||
import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
|
||||
import { getCurrentBranch } from '@pnpm/git-utils'
|
||||
import { createMatcher } from '@pnpm/matcher'
|
||||
import { type ProjectManifest } from '@pnpm/types'
|
||||
import betterPathResolve from 'better-path-resolve'
|
||||
import camelcase from 'camelcase'
|
||||
import isWindows from 'is-windows'
|
||||
@@ -20,7 +21,8 @@ import normalizeRegistryUrl from 'normalize-registry-url'
|
||||
import realpathMissing from 'realpath-missing'
|
||||
import pathAbsolute from 'path-absolute'
|
||||
import which from 'which'
|
||||
import { inheritAuthConfig, isSupportedNpmConfig, pickNpmAuthConfig } from './auth.js'
|
||||
import { inheritAuthConfig, isIniConfigKey, pickIniConfig } from './auth.js'
|
||||
import { isConfigFileKey } from './configFileKey.js'
|
||||
import { checkGlobalBinDir } from './checkGlobalBinDir.js'
|
||||
import { hasDependencyBuildOptions, extractAndRemoveDependencyBuildOptions } from './dependencyBuildOptions.js'
|
||||
import { getNetworkConfigs } from './getNetworkConfigs.js'
|
||||
@@ -35,7 +37,7 @@ import {
|
||||
} from './Config.js'
|
||||
import { getDefaultWorkspaceConcurrency, getWorkspaceConcurrency } from './concurrency.js'
|
||||
import { parseEnvVars } from './env.js'
|
||||
import { readWorkspaceManifest } from '@pnpm/workspace.read-manifest'
|
||||
import { type WorkspaceManifest, readWorkspaceManifest } from '@pnpm/workspace.read-manifest'
|
||||
|
||||
import { types } from './types.js'
|
||||
import { getOptionsFromPnpmSettings, getOptionsFromRootManifest } from './getOptionsFromRootManifest.js'
|
||||
@@ -51,6 +53,9 @@ export { getDefaultWorkspaceConcurrency, getWorkspaceConcurrency } from './concu
|
||||
|
||||
export type { Config, UniversalOptions, WantedPackageManager, VerifyDepsBeforeRun }
|
||||
|
||||
export { isIniConfigKey } from './auth.js'
|
||||
export { type ConfigFileKey, isConfigFileKey } from './configFileKey.js'
|
||||
|
||||
type CamelToKebabCase<S extends string> = S extends `${infer T}${infer U}`
|
||||
? `${T extends Capitalize<T> ? '-' : ''}${Lowercase<T>}${CamelToKebabCase<U>}`
|
||||
: S
|
||||
@@ -244,9 +249,10 @@ export async function getConfig (opts: {
|
||||
rcOptions
|
||||
.map((configKey) => [
|
||||
camelcase(configKey, { locale: 'en-US' }),
|
||||
isSupportedNpmConfig(configKey) ? npmConfig.get(configKey) : (defaultOptions as Record<string, unknown>)[configKey],
|
||||
isIniConfigKey(configKey) ? npmConfig.get(configKey) : (defaultOptions as Record<string, unknown>)[configKey],
|
||||
])
|
||||
) as ConfigWithDeprecatedSettings
|
||||
|
||||
const globalDepsBuildConfig = extractAndRemoveDependencyBuildOptions(pnpmConfig)
|
||||
|
||||
Object.assign(pnpmConfig, configFromCliOpts)
|
||||
@@ -271,12 +277,27 @@ export async function getConfig (opts: {
|
||||
: `${packageManager.name}/${packageManager.version} npm/? node/${process.version} ${process.platform} ${process.arch}`
|
||||
pnpmConfig.rawConfig = Object.assign(
|
||||
{},
|
||||
...npmConfig.list.map(pickNpmAuthConfig).reverse(),
|
||||
pickNpmAuthConfig(cliOptions),
|
||||
...npmConfig.list.map(pickIniConfig).reverse(),
|
||||
pickIniConfig(cliOptions),
|
||||
{ 'user-agent': pnpmConfig.userAgent },
|
||||
{ globalconfig: path.join(configDir, 'rc') },
|
||||
{ 'npm-globalconfig': npmDefaults.globalconfig }
|
||||
)
|
||||
|
||||
const globalYamlConfig = await readWorkspaceManifest(configDir, GLOBAL_CONFIG_YAML_FILENAME)
|
||||
for (const key in globalYamlConfig) {
|
||||
if (!isConfigFileKey(kebabCase(key))) {
|
||||
delete globalYamlConfig[key as keyof typeof globalYamlConfig]
|
||||
}
|
||||
}
|
||||
if (globalYamlConfig) {
|
||||
addSettingsFromWorkspaceManifestToConfig(pnpmConfig, {
|
||||
configFromCliOpts,
|
||||
projectManifest: undefined,
|
||||
workspaceDir: undefined,
|
||||
workspaceManifest: globalYamlConfig,
|
||||
})
|
||||
}
|
||||
const networkConfigs = getNetworkConfigs(pnpmConfig.rawConfig)
|
||||
pnpmConfig.registries = {
|
||||
default: normalizeRegistryUrl(pnpmConfig.rawConfig.registry),
|
||||
@@ -370,29 +391,12 @@ export async function getConfig (opts: {
|
||||
|
||||
pnpmConfig.workspacePackagePatterns = cliOptions['workspace-packages'] as string[] ?? workspaceManifest?.packages
|
||||
if (workspaceManifest) {
|
||||
const newSettings = Object.assign(getOptionsFromPnpmSettings(pnpmConfig.workspaceDir, workspaceManifest, pnpmConfig.rootProjectManifest), configFromCliOpts)
|
||||
for (const [key, value] of Object.entries(newSettings)) {
|
||||
if (!isCamelCase(key)) continue
|
||||
|
||||
// @ts-expect-error
|
||||
pnpmConfig[key] = value
|
||||
|
||||
const kebabKey = kebabCase(key)
|
||||
// Q: Why `types` instead of `rcOptionTypes`?
|
||||
// A: `rcOptionTypes` includes options that would matter to the `npm` cli which wouldn't care about `pnpm-workspace.yaml`.
|
||||
const targetKey = kebabKey in types ? kebabKey : key
|
||||
pnpmConfig.rawConfig[targetKey] = value
|
||||
}
|
||||
// All the pnpm_config_ env variables should override the settings from pnpm-workspace.yaml,
|
||||
// as it happens with .npmrc.
|
||||
// Until that is fixed, we should at the very least keep the right priority for verifyDepsBeforeRun,
|
||||
// or else, we'll get infinite recursion.
|
||||
// Related issue: https://github.com/pnpm/pnpm/issues/10060
|
||||
if (process.env.pnpm_config_verify_deps_before_run != null) {
|
||||
pnpmConfig.verifyDepsBeforeRun = process.env.pnpm_config_verify_deps_before_run as VerifyDepsBeforeRun
|
||||
pnpmConfig.rawConfig['verify-deps-before-run'] = pnpmConfig.verifyDepsBeforeRun
|
||||
}
|
||||
pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
|
||||
addSettingsFromWorkspaceManifestToConfig(pnpmConfig, {
|
||||
configFromCliOpts,
|
||||
projectManifest: pnpmConfig.rootProjectManifest,
|
||||
workspaceDir: pnpmConfig.workspaceDir,
|
||||
workspaceManifest,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -638,3 +642,40 @@ function parsePackageManager (packageManager: string): { name: string, version:
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
function addSettingsFromWorkspaceManifestToConfig (pnpmConfig: Config, {
|
||||
configFromCliOpts,
|
||||
projectManifest,
|
||||
workspaceManifest,
|
||||
workspaceDir,
|
||||
}: {
|
||||
configFromCliOpts: Record<string, unknown>
|
||||
projectManifest: ProjectManifest | undefined
|
||||
workspaceDir: string | undefined
|
||||
workspaceManifest: WorkspaceManifest
|
||||
}): void {
|
||||
const newSettings = Object.assign(getOptionsFromPnpmSettings(workspaceDir, workspaceManifest, projectManifest), configFromCliOpts)
|
||||
for (const [key, value] of Object.entries(newSettings)) {
|
||||
if (!isCamelCase(key)) continue
|
||||
|
||||
// @ts-expect-error
|
||||
pnpmConfig[key] = value
|
||||
|
||||
const kebabKey = kebabCase(key)
|
||||
// Q: Why `types` instead of `rcOptionTypes`?
|
||||
// A: `rcOptionTypes` includes options that would matter to the `npm` cli which wouldn't care about `pnpm-workspace.yaml`.
|
||||
const isRc = kebabKey in types
|
||||
const targetKey = isRc ? kebabKey : key
|
||||
pnpmConfig.rawConfig[targetKey] = value
|
||||
}
|
||||
// All the pnpm_config_ env variables should override the settings from pnpm-workspace.yaml,
|
||||
// as it happens with .npmrc.
|
||||
// Until that is fixed, we should at the very least keep the right priority for verifyDepsBeforeRun,
|
||||
// or else, we'll get infinite recursion.
|
||||
// Related issue: https://github.com/pnpm/pnpm/issues/10060
|
||||
if (process.env.pnpm_config_verify_deps_before_run != null) {
|
||||
pnpmConfig.verifyDepsBeforeRun = process.env.pnpm_config_verify_deps_before_run as VerifyDepsBeforeRun
|
||||
pnpmConfig.rawConfig['verify-deps-before-run'] = pnpmConfig.verifyDepsBeforeRun
|
||||
}
|
||||
pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import npmTypes from '@pnpm/npm-conf/lib/types.js'
|
||||
import { type TrustPolicy } from '@pnpm/types'
|
||||
|
||||
export const types = Object.assign({
|
||||
export const pnpmTypes = {
|
||||
'auto-install-peers': Boolean,
|
||||
bail: Boolean,
|
||||
ci: Boolean,
|
||||
@@ -139,4 +139,14 @@ export const types = Object.assign({
|
||||
cpu: [String, Array],
|
||||
libc: [String, Array],
|
||||
os: [String, Array],
|
||||
}, npmTypes.types)
|
||||
}
|
||||
|
||||
// NOTE: There is an oversight I just now notice thanks to a test failure: pnpmTypes (which used to be the object literal inside `Object.assign`)
|
||||
// contains some field that overlaps with that of `npmTypes.types`. The definitions of such fields are pointless as they are overwritten by
|
||||
// `npmTypes.types` anyway.
|
||||
// TODO: Fix this overlap later.
|
||||
// TODO: After that, move `...pnpmTypes` down, `...npmTypes.types` up.
|
||||
export const types = {
|
||||
...pnpmTypes,
|
||||
...npmTypes.types,
|
||||
}
|
||||
|
||||
@@ -1358,3 +1358,44 @@ test('CLI should override environment variable pnpm_config_*', async () => {
|
||||
'use-node-version': '22.0.0',
|
||||
})).toBe('22.0.0')
|
||||
})
|
||||
|
||||
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 })
|
||||
writeYamlFile('.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'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"dependencies": {
|
||||
"@pnpm/cli-utils": "workspace:*",
|
||||
"@pnpm/config": "workspace:*",
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/naming-cases": "workspace:*",
|
||||
"@pnpm/object.key-sorting": "workspace:*",
|
||||
@@ -45,7 +46,8 @@
|
||||
"lodash.kebabcase": "catalog:",
|
||||
"read-ini-file": "catalog:",
|
||||
"render-help": "catalog:",
|
||||
"write-ini-file": "catalog:"
|
||||
"write-ini-file": "catalog:",
|
||||
"write-yaml-file": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pnpm/logger": "catalog:"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from 'path'
|
||||
import util from 'util'
|
||||
import { types } from '@pnpm/config'
|
||||
import { type ConfigFileKey, types, isConfigFileKey } from '@pnpm/config'
|
||||
import { GLOBAL_CONFIG_YAML_FILENAME, WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { isCamelCase, isStrictlyKebabCase } from '@pnpm/naming-cases'
|
||||
import { parsePropertyPath } from '@pnpm/object.property-path'
|
||||
@@ -11,7 +12,7 @@ import kebabCase from 'lodash.kebabcase'
|
||||
import { readIniFile } from 'read-ini-file'
|
||||
import { writeIniFile } from 'write-ini-file'
|
||||
import { type ConfigCommandOptions } from './ConfigCommandOptions.js'
|
||||
import { getConfigFilePath } from './getConfigFilePath.js'
|
||||
import { getConfigFileInfo } from './getConfigFileInfo.js'
|
||||
import { settingShouldFallBackToNpm } from './settingShouldFallBackToNpm.js'
|
||||
|
||||
export async function configSet (opts: ConfigCommandOptions, key: string, valueParam: string | null): Promise<void> {
|
||||
@@ -56,18 +57,29 @@ export async function configSet (opts: ConfigCommandOptions, key: string, valueP
|
||||
}
|
||||
}
|
||||
|
||||
const { configPath, isWorkspaceYaml } = getConfigFilePath(opts)
|
||||
const { configDir, configFileName } = getConfigFileInfo(key, opts)
|
||||
const configPath = path.join(configDir, configFileName)
|
||||
|
||||
if (isWorkspaceYaml) {
|
||||
switch (configFileName) {
|
||||
case GLOBAL_CONFIG_YAML_FILENAME:
|
||||
case WORKSPACE_MANIFEST_FILENAME: {
|
||||
if (configFileName === GLOBAL_CONFIG_YAML_FILENAME) {
|
||||
key = validateYamlConfigKey(key)
|
||||
}
|
||||
key = validateWorkspaceKey(key)
|
||||
await updateWorkspaceManifest(opts.workspaceDir ?? opts.dir, {
|
||||
await updateWorkspaceManifest(configDir, {
|
||||
fileName: configFileName,
|
||||
updatedFields: ({
|
||||
[key]: castField(value, kebabCase(key)),
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
case 'rc':
|
||||
case '.npmrc': {
|
||||
const settings = await safeReadIniFile(configPath)
|
||||
key = validateRcKey(key)
|
||||
key = validateIniConfigKey(key)
|
||||
if (value == null) {
|
||||
if (settings[key] == null) return
|
||||
delete settings[key]
|
||||
@@ -75,6 +87,13 @@ export async function configSet (opts: ConfigCommandOptions, key: string, valueP
|
||||
settings[key] = value
|
||||
}
|
||||
await writeIniFile(configPath, settings)
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
const _typeGuard: never = configFileName
|
||||
throw new Error(`Unhandled case: ${JSON.stringify(_typeGuard)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,10 +164,10 @@ function validateSimpleKey (key: string): string {
|
||||
return first.value.toString()
|
||||
}
|
||||
|
||||
export class ConfigSetUnsupportedRcKeyError extends PnpmError {
|
||||
export class ConfigSetUnsupportedIniConfigKeyError extends PnpmError {
|
||||
readonly key: string
|
||||
constructor (key: string) {
|
||||
super('CONFIG_SET_UNSUPPORTED_RC_KEY', `Key ${JSON.stringify(key)} isn't supported by rc files`, {
|
||||
super('CONFIG_SET_UNSUPPORTED_INI_CONFIG_KEY', `Key ${JSON.stringify(key)} isn't supported by INI config files`, {
|
||||
hint: `Add ${JSON.stringify(camelCase(key))} to the project workspace manifest instead`,
|
||||
})
|
||||
this.key = key
|
||||
@@ -156,16 +175,20 @@ export class ConfigSetUnsupportedRcKeyError extends PnpmError {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the kebab-case of {@link key} is supported by rc files.
|
||||
* Validate whether the kebab-case of {@link key} is supported by INI config files.
|
||||
*
|
||||
* Return the kebab-case if it is, throw an error otherwise.
|
||||
*
|
||||
* "INI config files" includes:
|
||||
* * The global INI config file named `rc`.
|
||||
* * The local INI config file named `.npmrc`.
|
||||
*/
|
||||
function validateRcKey (key: string): string {
|
||||
function validateIniConfigKey (key: string): string {
|
||||
const kebabKey = kebabCase(key)
|
||||
if (kebabKey in types) {
|
||||
return kebabKey
|
||||
}
|
||||
throw new ConfigSetUnsupportedRcKeyError(key)
|
||||
throw new ConfigSetUnsupportedIniConfigKeyError(key)
|
||||
}
|
||||
|
||||
export class ConfigSetUnsupportedWorkspaceKeyError extends PnpmError {
|
||||
@@ -197,3 +220,26 @@ async function safeReadIniFile (configPath: string): Promise<Record<string, unkn
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigSetUnsupportedYamlConfigKeyError extends PnpmError {
|
||||
readonly key: string
|
||||
constructor (key: string) {
|
||||
super('CONFIG_SET_UNSUPPORTED_YAML_CONFIG_KEY', `The key ${JSON.stringify(key)} isn't supported by the global config.yaml file`, {
|
||||
hint: 'Try setting them instead to the local pnpm-workspace.yaml file',
|
||||
})
|
||||
this.key = key
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the {@link key} is allowed in the global config.yaml file.
|
||||
*
|
||||
* Return the kebab-case if it is, throw an error otherwise.
|
||||
*/
|
||||
function validateYamlConfigKey (key: string): ConfigFileKey {
|
||||
const kebabKey = kebabCase(key)
|
||||
if (!isConfigFileKey(kebabKey)) {
|
||||
throw new ConfigSetUnsupportedYamlConfigKeyError(key)
|
||||
}
|
||||
return kebabKey
|
||||
}
|
||||
|
||||
33
config/plugin-commands-config/src/getConfigFileInfo.ts
Normal file
33
config/plugin-commands-config/src/getConfigFileInfo.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import kebabCase from 'lodash.kebabcase'
|
||||
import { isIniConfigKey } from '@pnpm/config'
|
||||
import { GLOBAL_CONFIG_YAML_FILENAME, WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
|
||||
import { type ConfigCommandOptions } from './ConfigCommandOptions.js'
|
||||
|
||||
export type ConfigFileName =
|
||||
| 'rc'
|
||||
| '.npmrc'
|
||||
| typeof GLOBAL_CONFIG_YAML_FILENAME
|
||||
| typeof WORKSPACE_MANIFEST_FILENAME
|
||||
|
||||
export interface ConfigFilePathInfo {
|
||||
configDir: string
|
||||
configFileName: ConfigFileName
|
||||
}
|
||||
|
||||
export function getConfigFileInfo (key: string, opts: Pick<ConfigCommandOptions, 'global' | 'configDir' | 'dir'>): ConfigFilePathInfo {
|
||||
key = kebabCase(key)
|
||||
|
||||
const configDir = opts.global ? opts.configDir : opts.dir
|
||||
|
||||
if (isIniConfigKey(key)) {
|
||||
// NOTE: The following code no longer does what the merged PR at <https://github.com/pnpm/pnpm/pull/10073> wants to do,
|
||||
// but considering the settings are now clearly divided into 2 separate categories, it should no longer be relevant.
|
||||
// TODO: Auth, network, and proxy settings should belong only to INI files.
|
||||
// Add more settings to `isIniConfigKey` to make it complete.
|
||||
const configFileName = opts.global ? 'rc' : '.npmrc'
|
||||
return { configDir, configFileName }
|
||||
} else {
|
||||
const configFileName = opts.global ? GLOBAL_CONFIG_YAML_FILENAME : WORKSPACE_MANIFEST_FILENAME
|
||||
return { configDir, configFileName }
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { type ConfigCommandOptions } from './ConfigCommandOptions.js'
|
||||
|
||||
interface ConfigFilePathInfo {
|
||||
configPath: string
|
||||
isWorkspaceYaml: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority: pnpm-workspace.yaml > .npmrc > default to pnpm-workspace.yaml
|
||||
*/
|
||||
export function getConfigFilePath (opts: Pick<ConfigCommandOptions, 'global' | 'configDir' | 'dir'>): ConfigFilePathInfo {
|
||||
if (opts.global) {
|
||||
return {
|
||||
configPath: path.join(opts.configDir, 'rc'),
|
||||
isWorkspaceYaml: false,
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceYamlPath = path.join(opts.dir, 'pnpm-workspace.yaml')
|
||||
if (fs.existsSync(workspaceYamlPath)) {
|
||||
return {
|
||||
configPath: workspaceYamlPath,
|
||||
isWorkspaceYaml: true,
|
||||
}
|
||||
}
|
||||
|
||||
const npmrcPath = path.join(opts.dir, '.npmrc')
|
||||
if (fs.existsSync(npmrcPath)) {
|
||||
return {
|
||||
configPath: npmrcPath,
|
||||
isWorkspaceYaml: false,
|
||||
}
|
||||
}
|
||||
|
||||
// If neither exists, return pnpm-workspace.yaml
|
||||
return {
|
||||
configPath: workspaceYamlPath,
|
||||
isWorkspaceYaml: true,
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// NOTE: The logic may be duplicated with `isIniConfigKey` from `@pnpm/config`,
|
||||
// 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) ||
|
||||
|
||||
@@ -3,13 +3,90 @@ import path from 'path'
|
||||
import { tempDir } from '@pnpm/prepare'
|
||||
import { config } from '@pnpm/plugin-commands-config'
|
||||
import { readIniFileSync } from 'read-ini-file'
|
||||
import { sync as readYamlFile } from 'read-yaml-file'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
|
||||
test('config delete', async () => {
|
||||
test('config delete on registry key not 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
|
||||
cache-dir=~/cache`)
|
||||
fs.writeFileSync(path.join(configDir, 'rc'), '@my-company:registry=https://registry.my-company.example.com/')
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['delete', 'registry'])
|
||||
|
||||
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({
|
||||
'@my-company:registry': 'https://registry.my-company.example.com/',
|
||||
})
|
||||
})
|
||||
|
||||
test('config delete on registry key set', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(configDir, 'rc'), 'registry=https://registry.my-company.example.com/')
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['delete', 'registry'])
|
||||
|
||||
expect(fs.readdirSync(configDir)).not.toContain('rc')
|
||||
})
|
||||
|
||||
test('config delete on npm-compatible key not set', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(configDir, 'rc'), '@my-company:registry=https://registry.my-company.example.com/')
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['delete', 'cafile'])
|
||||
|
||||
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({
|
||||
'@my-company:registry': 'https://registry.my-company.example.com/',
|
||||
})
|
||||
})
|
||||
|
||||
test('config delete on npm-compatible key set', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(configDir, 'rc'), 'cafile=some-cafile')
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['delete', 'cafile'])
|
||||
|
||||
// NOTE: pnpm currently does not delete empty rc files.
|
||||
// TODO: maybe we should?
|
||||
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({})
|
||||
})
|
||||
|
||||
test('config delete on pnpm-specific key not set', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
writeYamlFile(path.join(configDir, 'config.yaml'), {
|
||||
cacheDir: '~/cache',
|
||||
})
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -19,7 +96,26 @@ cache-dir=~/cache`)
|
||||
rawConfig: {},
|
||||
}, ['delete', 'store-dir'])
|
||||
|
||||
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({
|
||||
'cache-dir': '~/cache',
|
||||
expect(readYamlFile(path.join(configDir, 'config.yaml'))).toStrictEqual({
|
||||
cacheDir: '~/cache',
|
||||
})
|
||||
})
|
||||
|
||||
test('config delete on pnpm-specific key set', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
writeYamlFile(path.join(configDir, 'config.yaml'), {
|
||||
cacheDir: '~/cache',
|
||||
})
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['delete', 'cache-dir'])
|
||||
|
||||
expect(fs.readdirSync(configDir)).not.toContain('config.yaml')
|
||||
})
|
||||
|
||||
@@ -5,12 +5,86 @@ import { tempDir } from '@pnpm/prepare'
|
||||
import { config } from '@pnpm/plugin-commands-config'
|
||||
import { readIniFileSync } from 'read-ini-file'
|
||||
import { sync as readYamlFile } from 'read-yaml-file'
|
||||
import { type ConfigFilesData, readConfigFiles, writeConfigFiles } from './utils/index.js'
|
||||
|
||||
test('config set using the global option', async () => {
|
||||
test('config set registry setting using the global option', 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')
|
||||
const initConfig = {
|
||||
globalRc: {
|
||||
'@jsr:registry': 'https://alternate-jsr.example.com/',
|
||||
},
|
||||
globalYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['set', 'registry', 'https://npm-registry.example.com/'])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalRc: {
|
||||
...initConfig.globalRc,
|
||||
registry: 'https://npm-registry.example.com/',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config set npm-compatible setting using the global option', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: {
|
||||
'@jsr:registry': 'https://alternate-jsr.example.com/',
|
||||
},
|
||||
globalYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: true,
|
||||
rawConfig: {},
|
||||
}, ['set', 'cafile', 'some-cafile'])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalRc: {
|
||||
...initConfig.globalRc,
|
||||
cafile: 'some-cafile',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config set pnpm-specific key using the global option', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: {
|
||||
'@jsr:registry': 'https://alternate-jsr.example.com/',
|
||||
},
|
||||
globalYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -20,17 +94,29 @@ test('config set using the global option', async () => {
|
||||
rawConfig: {},
|
||||
}, ['set', 'fetch-retries', '1'])
|
||||
|
||||
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({
|
||||
'store-dir': '~/store',
|
||||
'fetch-retries': '1',
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalYaml: {
|
||||
...initConfig.globalYaml,
|
||||
fetchRetries: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config set using the location=global option', 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')
|
||||
const initConfig = {
|
||||
globalRc: {
|
||||
'@jsr:registry': 'https://alternate-jsr.example.com/',
|
||||
},
|
||||
globalYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -40,16 +126,29 @@ test('config set using the location=global option', async () => {
|
||||
rawConfig: {},
|
||||
}, ['set', 'fetchRetries', '1'])
|
||||
|
||||
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({
|
||||
'store-dir': '~/store',
|
||||
'fetch-retries': '1',
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalYaml: {
|
||||
...initConfig.globalYaml,
|
||||
fetchRetries: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config set using the location=project option. The setting is written to pnpm-workspace.yaml, when .npmrc is not present', async () => {
|
||||
test('config set pnpm-specific setting using the location=project option', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
const initConfig = {
|
||||
globalRc: undefined,
|
||||
globalYaml: undefined,
|
||||
localRc: {
|
||||
'@jsr:registry': 'https://alternate-jsr.example.com/',
|
||||
},
|
||||
localYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -59,12 +158,16 @@ test('config set using the location=project option. The setting is written to pn
|
||||
rawConfig: {},
|
||||
}, ['set', 'virtual-store-dir', '.pnpm'])
|
||||
|
||||
expect(readYamlFile(path.join(tmp, 'pnpm-workspace.yaml'))).toEqual({
|
||||
virtualStoreDir: '.pnpm',
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
localYaml: {
|
||||
...initConfig.localYaml,
|
||||
virtualStoreDir: '.pnpm',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config delete using the location=project option. The setting in pnpm-workspace.yaml will be deleted, when .npmrc is not present', async () => {
|
||||
test('config delete with location=project, when delete the last setting from pnpm-workspace.yaml, would delete the file itself', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
@@ -92,11 +195,21 @@ test('config delete using the location=project option. The setting in pnpm-works
|
||||
expect(fs.existsSync(path.join(tmp, 'pnpm-workspace.yaml'))).toBeFalsy()
|
||||
})
|
||||
|
||||
test('config set using the location=project option', async () => {
|
||||
test('config set registry setting using the location=project option', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(tmp, '.npmrc'), 'store-dir=~/store')
|
||||
const initConfig = {
|
||||
globalRc: undefined,
|
||||
globalYaml: undefined,
|
||||
localRc: {
|
||||
'@jsr:registry': 'https://alternate-jsr.example.com/',
|
||||
},
|
||||
localYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -104,11 +217,47 @@ test('config set using the location=project option', async () => {
|
||||
configDir,
|
||||
location: 'project',
|
||||
rawConfig: {},
|
||||
}, ['set', 'fetch-retries', '1'])
|
||||
}, ['set', 'registry', 'https://npm-registry.example.com/'])
|
||||
|
||||
expect(readIniFileSync(path.join(tmp, '.npmrc'))).toEqual({
|
||||
'store-dir': '~/store',
|
||||
'fetch-retries': '1',
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
localRc: {
|
||||
...initConfig.localRc,
|
||||
registry: 'https://npm-registry.example.com/',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config set npm-compatible setting using the location=project option', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
const initConfig = {
|
||||
globalRc: undefined,
|
||||
globalYaml: undefined,
|
||||
localRc: {
|
||||
'@jsr:registry': 'https://alternate-jsr.example.com/',
|
||||
},
|
||||
localYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
location: 'project',
|
||||
rawConfig: {},
|
||||
}, ['set', 'cafile', 'some-cafile'])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
localRc: {
|
||||
...initConfig.localRc,
|
||||
cafile: 'some-cafile',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -130,10 +279,98 @@ test('config set saves the setting in the right format to pnpm-workspace.yaml',
|
||||
})
|
||||
})
|
||||
|
||||
test('config set in project .npmrc file', async () => {
|
||||
test('config set registry setting in project .npmrc file', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.writeFileSync(path.join(tmp, '.npmrc'), 'store-dir=~/store')
|
||||
const initConfig = {
|
||||
globalRc: {
|
||||
'@my-company:registry': 'https://registry.my-company.example.com/',
|
||||
},
|
||||
globalYaml: {
|
||||
onlyBuiltDependencies: ['foo', 'bar'],
|
||||
},
|
||||
localRc: {
|
||||
'@local:registry': 'https://localhost:7777/',
|
||||
},
|
||||
localYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: false,
|
||||
location: 'project',
|
||||
rawConfig: {},
|
||||
}, ['set', 'registry', 'https://npm-registry.example.com/'])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
localRc: {
|
||||
...initConfig.localRc,
|
||||
registry: 'https://npm-registry.example.com/',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config set npm-compatible setting in project .npmrc file', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: {
|
||||
'@my-company:registry': 'https://registry.my-company.example.com/',
|
||||
},
|
||||
globalYaml: {
|
||||
onlyBuiltDependencies: ['foo', 'bar'],
|
||||
},
|
||||
localRc: {
|
||||
'@local:registry': 'https://localhost:7777/',
|
||||
},
|
||||
localYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
global: false,
|
||||
location: 'project',
|
||||
rawConfig: {},
|
||||
}, ['set', 'cafile', 'some-cafile'])
|
||||
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
localRc: {
|
||||
...initConfig.localRc,
|
||||
cafile: 'some-cafile',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config set pnpm-specific setting in project pnpm-workspace.yaml file', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
const initConfig = {
|
||||
globalRc: {
|
||||
'@my-company:registry': 'https://registry.my-company.example.com/',
|
||||
},
|
||||
globalYaml: {
|
||||
onlyBuiltDependencies: ['foo', 'bar'],
|
||||
},
|
||||
localRc: {
|
||||
'@local:registry': 'https://localhost:7777/',
|
||||
},
|
||||
localYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -144,17 +381,33 @@ test('config set in project .npmrc file', async () => {
|
||||
rawConfig: {},
|
||||
}, ['set', 'fetch-retries', '1'])
|
||||
|
||||
expect(readIniFileSync(path.join(tmp, '.npmrc'))).toEqual({
|
||||
'store-dir': '~/store',
|
||||
'fetch-retries': '1',
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
localYaml: {
|
||||
...initConfig.localYaml,
|
||||
fetchRetries: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config set key=value', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(tmp, '.npmrc'), 'store-dir=~/store')
|
||||
const initConfig = {
|
||||
globalRc: {
|
||||
'@my-company:registry': 'https://registry.my-company.example.com/',
|
||||
},
|
||||
globalYaml: {
|
||||
onlyBuiltDependencies: ['foo', 'bar'],
|
||||
},
|
||||
localRc: {
|
||||
'@local:registry': 'https://localhost:7777/',
|
||||
},
|
||||
localYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -164,17 +417,33 @@ test('config set key=value', async () => {
|
||||
rawConfig: {},
|
||||
}, ['set', 'fetch-retries=1'])
|
||||
|
||||
expect(readIniFileSync(path.join(tmp, '.npmrc'))).toEqual({
|
||||
'store-dir': '~/store',
|
||||
'fetch-retries': '1',
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
localYaml: {
|
||||
...initConfig.localYaml,
|
||||
fetchRetries: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config set key=value, when value contains a "="', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(tmp, '.npmrc'), 'store-dir=~/store')
|
||||
const initConfig = {
|
||||
globalRc: {
|
||||
'@my-company:registry': 'https://registry.my-company.example.com/',
|
||||
},
|
||||
globalYaml: {
|
||||
onlyBuiltDependencies: ['foo', 'bar'],
|
||||
},
|
||||
localRc: {
|
||||
'@local:registry': 'https://localhost:7777/',
|
||||
},
|
||||
localYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -184,9 +453,12 @@ test('config set key=value, when value contains a "="', async () => {
|
||||
rawConfig: {},
|
||||
}, ['set', 'lockfile-dir=foo=bar'])
|
||||
|
||||
expect(readIniFileSync(path.join(tmp, '.npmrc'))).toEqual({
|
||||
'store-dir': '~/store',
|
||||
'lockfile-dir': 'foo=bar',
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
localYaml: {
|
||||
...initConfig.localYaml,
|
||||
lockfileDir: 'foo=bar',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -216,8 +488,15 @@ test('config set or delete throws missing params error', async () => {
|
||||
test('config set with dot leading key', 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')
|
||||
const initConfig = {
|
||||
globalRc: undefined,
|
||||
globalYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -227,17 +506,27 @@ test('config set with dot leading key', async () => {
|
||||
rawConfig: {},
|
||||
}, ['set', '.fetchRetries', '1'])
|
||||
|
||||
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({
|
||||
'store-dir': '~/store',
|
||||
'fetch-retries': '1',
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalYaml: {
|
||||
...initConfig.globalYaml,
|
||||
fetchRetries: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('config set with subscripted key', 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')
|
||||
const initConfig = {
|
||||
globalRc: undefined,
|
||||
globalYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -247,9 +536,12 @@ test('config set with subscripted key', async () => {
|
||||
rawConfig: {},
|
||||
}, ['set', '["fetch-retries"]', '1'])
|
||||
|
||||
expect(readIniFileSync(path.join(configDir, 'rc'))).toEqual({
|
||||
'store-dir': '~/store',
|
||||
'fetch-retries': '1',
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
globalYaml: {
|
||||
...initConfig.globalYaml,
|
||||
fetchRetries: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -332,8 +624,15 @@ test('config set with location=project and json=true', async () => {
|
||||
test('config set refuses writing workspace-specific settings to the global config', 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')
|
||||
const initConfig = {
|
||||
globalRc: undefined,
|
||||
globalYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
localRc: undefined,
|
||||
localYaml: undefined,
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await expect(config.handler({
|
||||
dir: process.cwd(),
|
||||
@@ -343,7 +642,7 @@ test('config set refuses writing workspace-specific settings to the global confi
|
||||
json: true,
|
||||
rawConfig: {},
|
||||
}, ['set', 'catalog', '{ "react": "19" }'])).rejects.toMatchObject({
|
||||
code: 'ERR_PNPM_CONFIG_SET_UNSUPPORTED_RC_KEY',
|
||||
code: 'ERR_PNPM_CONFIG_SET_UNSUPPORTED_YAML_CONFIG_KEY',
|
||||
key: 'catalog',
|
||||
})
|
||||
|
||||
@@ -366,7 +665,7 @@ test('config set refuses writing workspace-specific settings to the global confi
|
||||
},
|
||||
},
|
||||
})])).rejects.toMatchObject({
|
||||
code: 'ERR_PNPM_CONFIG_SET_UNSUPPORTED_RC_KEY',
|
||||
code: 'ERR_PNPM_CONFIG_SET_UNSUPPORTED_YAML_CONFIG_KEY',
|
||||
key: 'packageExtensions',
|
||||
})
|
||||
|
||||
@@ -389,37 +688,42 @@ test('config set refuses writing workspace-specific settings to the global confi
|
||||
},
|
||||
},
|
||||
})])).rejects.toMatchObject({
|
||||
code: 'ERR_PNPM_CONFIG_SET_UNSUPPORTED_RC_KEY',
|
||||
code: 'ERR_PNPM_CONFIG_SET_UNSUPPORTED_YAML_CONFIG_KEY',
|
||||
key: 'package-extensions',
|
||||
})
|
||||
})
|
||||
|
||||
test('config set refuses writing workspace-specific settings to .npmrc', async () => {
|
||||
test('config set writes workspace-specific settings to pnpm-workspace.yaml', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(tmp, '.npmrc'), 'store-dir=~/store')
|
||||
const initConfig = {
|
||||
globalRc: undefined,
|
||||
globalYaml: undefined,
|
||||
localRc: undefined,
|
||||
localYaml: {
|
||||
storeDir: '~/store',
|
||||
},
|
||||
} satisfies ConfigFilesData
|
||||
writeConfigFiles(configDir, tmp, initConfig)
|
||||
|
||||
await expect(config.handler({
|
||||
const catalog = { react: '19' }
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
location: 'project',
|
||||
json: true,
|
||||
rawConfig: {},
|
||||
}, ['set', 'catalog', '{ "react": "19" }'])).rejects.toMatchObject({
|
||||
code: 'ERR_PNPM_CONFIG_SET_UNSUPPORTED_RC_KEY',
|
||||
key: 'catalog',
|
||||
}, ['set', 'catalog', JSON.stringify(catalog)])
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
localYaml: {
|
||||
...initConfig.localYaml,
|
||||
catalog,
|
||||
},
|
||||
})
|
||||
|
||||
await expect(config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
location: 'project',
|
||||
json: true,
|
||||
rawConfig: {},
|
||||
}, ['set', 'packageExtensions', JSON.stringify({
|
||||
const packageExtensions = {
|
||||
'@babel/parser': {
|
||||
peerDependencies: {
|
||||
'@babel/types': '*',
|
||||
@@ -430,32 +734,22 @@ test('config set refuses writing workspace-specific settings to .npmrc', async (
|
||||
slash: '3',
|
||||
},
|
||||
},
|
||||
})])).rejects.toMatchObject({
|
||||
code: 'ERR_PNPM_CONFIG_SET_UNSUPPORTED_RC_KEY',
|
||||
key: 'packageExtensions',
|
||||
})
|
||||
|
||||
await expect(config.handler({
|
||||
}
|
||||
await config.handler({
|
||||
dir: process.cwd(),
|
||||
cliOptions: {},
|
||||
configDir,
|
||||
location: 'project',
|
||||
json: true,
|
||||
rawConfig: {},
|
||||
}, ['set', 'package-extensions', JSON.stringify({
|
||||
'@babel/parser': {
|
||||
peerDependencies: {
|
||||
'@babel/types': '*',
|
||||
},
|
||||
}, ['set', 'packageExtensions', JSON.stringify(packageExtensions)])
|
||||
expect(readConfigFiles(configDir, tmp)).toEqual({
|
||||
...initConfig,
|
||||
localYaml: {
|
||||
...initConfig.localYaml,
|
||||
catalog,
|
||||
packageExtensions,
|
||||
},
|
||||
'jest-circus': {
|
||||
dependencies: {
|
||||
slash: '3',
|
||||
},
|
||||
},
|
||||
})])).rejects.toMatchObject({
|
||||
code: 'ERR_PNPM_CONFIG_SET_UNSUPPORTED_RC_KEY',
|
||||
key: 'package-extensions',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -526,6 +820,8 @@ test('config set scoped registry with --location=project should create .npmrc',
|
||||
expect(fs.existsSync(path.join(tmp, 'pnpm-workspace.yaml'))).toBeFalsy()
|
||||
})
|
||||
|
||||
// NOTE: this test gives false positive since <https://github.com/pnpm/pnpm/pull/10145>.
|
||||
// TODO: fix this test.
|
||||
test('config set when both pnpm-workspace.yaml and .npmrc exist, pnpm-workspace.yaml has priority', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
@@ -550,6 +846,8 @@ test('config set when both pnpm-workspace.yaml and .npmrc exist, pnpm-workspace.
|
||||
})
|
||||
})
|
||||
|
||||
// NOTE: this test gives false positive since <https://github.com/pnpm/pnpm/pull/10145>.
|
||||
// TODO: fix this test.
|
||||
test('config set when only pnpm-workspace.yaml exists, writes to it', async () => {
|
||||
const tmp = tempDir()
|
||||
const configDir = path.join(tmp, 'global-config')
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { readIniFileSync } from 'read-ini-file'
|
||||
import { writeIniFileSync } from 'write-ini-file'
|
||||
import { sync as readYamlFile } from 'read-yaml-file'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
import { type config } from '../../src/index.js'
|
||||
|
||||
export function getOutputString (result: config.ConfigHandlerResult): string {
|
||||
@@ -7,3 +13,64 @@ export function getOutputString (result: config.ConfigHandlerResult): string {
|
||||
const _typeGuard: never = result // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
throw new Error('unreachable')
|
||||
}
|
||||
|
||||
export interface ConfigFilesData {
|
||||
globalRc: Record<string, unknown> | undefined
|
||||
globalYaml: Record<string, unknown> | undefined
|
||||
localRc: Record<string, unknown> | undefined
|
||||
localYaml: Record<string, unknown> | undefined
|
||||
}
|
||||
|
||||
export function readConfigFiles (globalConfigDir: string | undefined, localDir: string | undefined): ConfigFilesData {
|
||||
function tryRead<Return> (reader: () => Return): Return | undefined {
|
||||
try {
|
||||
return reader()
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
||||
return undefined
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
globalRc: globalConfigDir
|
||||
? tryRead(() => readIniFileSync(path.join(globalConfigDir, 'rc')) as Record<string, unknown>)
|
||||
: undefined,
|
||||
globalYaml: globalConfigDir
|
||||
? tryRead(() => readYamlFile(path.join(globalConfigDir, 'config.yaml')))
|
||||
: undefined,
|
||||
localRc: localDir
|
||||
? tryRead(() => readIniFileSync(path.join(localDir, '.npmrc')) as Record<string, unknown>)
|
||||
: undefined,
|
||||
localYaml: localDir
|
||||
? tryRead(() => readYamlFile(path.join(localDir, 'pnpm-workspace.yaml')))
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function writeConfigFiles (globalConfigDir: string | undefined, localDir: string | undefined, data: ConfigFilesData): void {
|
||||
if (globalConfigDir) {
|
||||
fs.mkdirSync(globalConfigDir, { recursive: true })
|
||||
|
||||
if (data.globalRc) {
|
||||
writeIniFileSync(path.join(globalConfigDir, 'rc'), data.globalRc)
|
||||
}
|
||||
|
||||
if (data.globalYaml) {
|
||||
writeYamlFile(path.join(globalConfigDir, 'config.yaml'), data.globalYaml)
|
||||
}
|
||||
}
|
||||
|
||||
if (localDir) {
|
||||
fs.mkdirSync(localDir, { recursive: true })
|
||||
|
||||
if (data.localRc) {
|
||||
writeIniFileSync(path.join(localDir, '.npmrc'), data.localRc)
|
||||
}
|
||||
|
||||
if (data.localYaml) {
|
||||
writeYamlFile(path.join(localDir, 'pnpm-workspace.yaml'), data.localYaml)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
{
|
||||
"path": "../../object/property-path"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/constants"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/error"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ export const ENGINE_NAME = `${process.platform};${process.arch};node${process.ve
|
||||
export const LAYOUT_VERSION = 5
|
||||
export const STORE_VERSION = 'v10'
|
||||
|
||||
export const GLOBAL_CONFIG_YAML_FILENAME = 'config.yaml'
|
||||
export const WORKSPACE_MANIFEST_FILENAME = 'pnpm-workspace.yaml'
|
||||
|
||||
// This file contains meta information
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -1944,6 +1944,9 @@ importers:
|
||||
'@pnpm/config':
|
||||
specifier: workspace:*
|
||||
version: link:../config
|
||||
'@pnpm/constants':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/constants
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/error
|
||||
@@ -1980,6 +1983,9 @@ importers:
|
||||
write-ini-file:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.1
|
||||
write-yaml-file:
|
||||
specifier: 'catalog:'
|
||||
version: 5.0.0
|
||||
devDependencies:
|
||||
'@jest/globals':
|
||||
specifier: 'catalog:'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
import { type WorkspaceManifest } from '@pnpm/workspace.read-manifest'
|
||||
import { prepare } from '@pnpm/prepare'
|
||||
@@ -228,3 +229,58 @@ test('pnpm config get "" gives exactly the same result as pnpm config list', ()
|
||||
expect(getResult.stdout.toString()).toBe(listResult.stdout.toString())
|
||||
}
|
||||
})
|
||||
|
||||
test('pnpm config get shows settings from global config.yaml', () => {
|
||||
prepare()
|
||||
|
||||
const XDG_CONFIG_HOME = path.resolve('.config')
|
||||
const configDir = path.join(XDG_CONFIG_HOME, 'pnpm')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
writeYamlFile(path.join(configDir, 'config.yaml'), {
|
||||
dangerouslyAllowAllBuilds: true,
|
||||
dlxCacheMaxAge: 1234,
|
||||
dev: true,
|
||||
frozenLockfile: true,
|
||||
catalog: {
|
||||
react: '^19.0.0',
|
||||
},
|
||||
packages: ['baz', 'qux'],
|
||||
packageExtensions: {
|
||||
'@babel/parser': {
|
||||
peerDependencies: {
|
||||
'@babel/types': '*',
|
||||
},
|
||||
},
|
||||
'jest-circus': {
|
||||
dependencies: {
|
||||
slash: '3',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const configGet = (key: string) => execPnpmSync(['config', 'get', key], {
|
||||
expectSuccess: true,
|
||||
env: {
|
||||
XDG_CONFIG_HOME,
|
||||
},
|
||||
}).stdout.toString().trim()
|
||||
|
||||
// lists keys that belong to global
|
||||
expect(configGet('dangerouslyAllowAllBuilds')).toBe('true')
|
||||
expect(configGet('dangerously-allow-all-builds')).toBe('true')
|
||||
expect(configGet('dlxCacheMaxAge')).toBe('1234')
|
||||
expect(configGet('dlx-cache-max-age')).toBe('1234')
|
||||
|
||||
// doesn't list CLI options
|
||||
expect(configGet('dev')).toBe('undefined')
|
||||
expect(configGet('frozenLockfile')).toBe('undefined')
|
||||
expect(configGet('frozen-lockfile')).toBe('undefined')
|
||||
|
||||
// doesn't list workspace-specific keys
|
||||
expect(configGet('catalog')).toBe('undefined')
|
||||
expect(configGet('catalogs')).toBe('undefined')
|
||||
expect(configGet('packages')).toBe('undefined')
|
||||
expect(configGet('packageExtensions')).toBe('undefined')
|
||||
expect(configGet('package-extensions')).toBe('undefined')
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
import { type Config } from '@pnpm/config'
|
||||
import { prepare } from '@pnpm/prepare'
|
||||
@@ -141,3 +142,60 @@ test('pnpm config list --json shows all keys in camelCase', () => {
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['only-built-dependencies'])
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['package-extensions'])
|
||||
})
|
||||
|
||||
test('pnpm config list shows settings from global config.yaml', () => {
|
||||
prepare()
|
||||
|
||||
const XDG_CONFIG_HOME = path.resolve('.config')
|
||||
const configDir = path.join(XDG_CONFIG_HOME, 'pnpm')
|
||||
fs.mkdirSync(configDir, { recursive: true })
|
||||
writeYamlFile(path.join(configDir, 'config.yaml'), {
|
||||
dangerouslyAllowAllBuilds: true,
|
||||
dlxCacheMaxAge: 1234,
|
||||
dev: true,
|
||||
frozenLockfile: true,
|
||||
catalog: {
|
||||
react: '^19.0.0',
|
||||
},
|
||||
packages: ['baz', 'qux'],
|
||||
packageExtensions: {
|
||||
'@babel/parser': {
|
||||
peerDependencies: {
|
||||
'@babel/types': '*',
|
||||
},
|
||||
},
|
||||
'jest-circus': {
|
||||
dependencies: {
|
||||
slash: '3',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { stdout } = execPnpmSync(['config', 'list'], {
|
||||
expectSuccess: true,
|
||||
env: {
|
||||
XDG_CONFIG_HOME,
|
||||
},
|
||||
})
|
||||
expect(JSON.parse(stdout.toString())).toStrictEqual(expect.objectContaining({
|
||||
dangerouslyAllowAllBuilds: true,
|
||||
dlxCacheMaxAge: 1234,
|
||||
}))
|
||||
|
||||
// doesn't list CLI options
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['dev'])
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['frozenLockfile'])
|
||||
|
||||
// doesn't list workspace-specific settings
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['catalog'])
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['catalogs'])
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['packages'])
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['packageExtensions'])
|
||||
|
||||
// doesn't list the kebab-case versions
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['frozen-lockfile'])
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['only-built-dependencies'])
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['dlx-cache-max-age'])
|
||||
expect(JSON.parse(stdout.toString())).not.toHaveProperty(['package-extensions'])
|
||||
})
|
||||
|
||||
@@ -20,20 +20,6 @@ test('switch to the pnpm version specified in the packageManager field of packag
|
||||
expect(stdout.toString()).toContain('Version 9.3.0')
|
||||
})
|
||||
|
||||
test('do not switch to the pnpm version specified in the packageManager field of package.json, if manage-package-manager-versions is set to false (backward-compatibility)', async () => {
|
||||
prepare()
|
||||
const pnpmHome = path.resolve('pnpm')
|
||||
const env = { PNPM_HOME: pnpmHome }
|
||||
fs.writeFileSync('.npmrc', 'manage-package-manager-versions=false')
|
||||
writeJsonFileSync('package.json', {
|
||||
packageManager: 'pnpm@9.3.0',
|
||||
})
|
||||
|
||||
const { stdout } = execPnpmSync(['help'], { env })
|
||||
|
||||
expect(stdout.toString()).not.toContain('Version 9.3.0')
|
||||
})
|
||||
|
||||
test('do not switch to the pnpm version specified in the packageManager field of package.json, if managePackageManagerVersions is set to false', async () => {
|
||||
prepare()
|
||||
const pnpmHome = path.resolve('pnpm')
|
||||
@@ -91,19 +77,17 @@ test('do not switch to pnpm version when a range is specified', async () => {
|
||||
|
||||
test('throws error if pnpm tools dir is corrupt', () => {
|
||||
prepare()
|
||||
const config = ['--config.manage-package-manager-versions=true'] as const
|
||||
const pnpmHome = path.resolve('pnpm')
|
||||
const env = { PNPM_HOME: pnpmHome }
|
||||
const version = '9.3.0'
|
||||
|
||||
// NOTE: replace this .npmrc file with an equivalent pnpm-workspace.yaml would cause the test to hang indefinitely.
|
||||
fs.writeFileSync('.npmrc', 'manage-package-manager-versions=true')
|
||||
|
||||
writeJsonFileSync('package.json', {
|
||||
packageManager: `pnpm@${version}`,
|
||||
})
|
||||
|
||||
// Run pnpm once to ensure the tools dir is created.
|
||||
execPnpmSync(['help'], { env })
|
||||
execPnpmSync([...config, 'help'], { env })
|
||||
|
||||
// Intentionally corrupt the tool dir.
|
||||
const toolDir = getToolDirPath({ pnpmHomeDir: pnpmHome, tool: { name: 'pnpm', version } })
|
||||
@@ -112,6 +96,6 @@ test('throws error if pnpm tools dir is corrupt', () => {
|
||||
fs.rmSync(path.join(toolDir, 'bin/pnpm.cmd'))
|
||||
}
|
||||
|
||||
const { stderr } = execPnpmSync(['help'], { env })
|
||||
const { stderr } = execPnpmSync([...config, 'help'], { env })
|
||||
expect(stderr.toString()).toContain('Failed to switch pnpm to v9.3.0. Looks like pnpm CLI is missing')
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'path'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { type ResolvedCatalogEntry } from '@pnpm/lockfile.types'
|
||||
import { readWorkspaceManifest, type WorkspaceManifest } from '@pnpm/workspace.read-manifest'
|
||||
import { WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
|
||||
import { type GLOBAL_CONFIG_YAML_FILENAME, WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
|
||||
import writeYamlFile from 'write-yaml-file'
|
||||
import { equals } from 'ramda'
|
||||
import { sortKeysByPriority } from '@pnpm/object.key-sorting'
|
||||
@@ -11,12 +11,18 @@ import {
|
||||
type Project,
|
||||
} from '@pnpm/types'
|
||||
|
||||
async function writeManifestFile (dir: string, manifest: Partial<WorkspaceManifest>): Promise<void> {
|
||||
export type FileName =
|
||||
| typeof GLOBAL_CONFIG_YAML_FILENAME
|
||||
| typeof WORKSPACE_MANIFEST_FILENAME
|
||||
|
||||
const DEFAULT_FILENAME: FileName = WORKSPACE_MANIFEST_FILENAME
|
||||
|
||||
async function writeManifestFile (dir: string, fileName: FileName, manifest: Partial<WorkspaceManifest>): Promise<void> {
|
||||
manifest = sortKeysByPriority({
|
||||
priority: { packages: 0 },
|
||||
deep: true,
|
||||
}, manifest)
|
||||
return writeYamlFile(path.join(dir, WORKSPACE_MANIFEST_FILENAME), manifest, {
|
||||
return writeYamlFile(path.join(dir, fileName), manifest, {
|
||||
lineWidth: -1, // This is setting line width to never wrap
|
||||
blankLines: true,
|
||||
noCompatMode: true,
|
||||
@@ -28,10 +34,12 @@ async function writeManifestFile (dir: string, manifest: Partial<WorkspaceManife
|
||||
export async function updateWorkspaceManifest (dir: string, opts: {
|
||||
updatedFields?: Partial<WorkspaceManifest>
|
||||
updatedCatalogs?: Catalogs
|
||||
fileName?: FileName
|
||||
cleanupUnusedCatalogs?: boolean
|
||||
allProjects?: Project[]
|
||||
}): Promise<void> {
|
||||
const manifest = await readWorkspaceManifest(dir) ?? {} as WorkspaceManifest
|
||||
const fileName = opts.fileName ?? DEFAULT_FILENAME
|
||||
const manifest = await readWorkspaceManifest(dir, fileName) ?? {} as WorkspaceManifest
|
||||
let shouldBeUpdated = opts.updatedCatalogs != null && addCatalogs(manifest, opts.updatedCatalogs)
|
||||
if (opts.cleanupUnusedCatalogs) {
|
||||
shouldBeUpdated = removePackagesFromWorkspaceCatalog(manifest, opts.allProjects ?? []) || shouldBeUpdated
|
||||
@@ -52,10 +60,10 @@ export async function updateWorkspaceManifest (dir: string, opts: {
|
||||
return
|
||||
}
|
||||
if (Object.keys(manifest).length === 0) {
|
||||
await fs.promises.rm(path.join(dir, WORKSPACE_MANIFEST_FILENAME))
|
||||
await fs.promises.rm(path.join(dir, fileName))
|
||||
return
|
||||
}
|
||||
await writeManifestFile(dir, manifest)
|
||||
await writeManifestFile(dir, fileName, manifest)
|
||||
}
|
||||
|
||||
export interface NewCatalogs {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import util from 'util'
|
||||
import { WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
|
||||
import { type GLOBAL_CONFIG_YAML_FILENAME, WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
|
||||
import { type PnpmSettings } from '@pnpm/types'
|
||||
import path from 'node:path'
|
||||
import readYamlFile from 'read-yaml-file'
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
} from './catalogs.js'
|
||||
import { InvalidWorkspaceManifestError } from './errors/InvalidWorkspaceManifestError.js'
|
||||
|
||||
export type ConfigFileName =
|
||||
| typeof GLOBAL_CONFIG_YAML_FILENAME
|
||||
| typeof WORKSPACE_MANIFEST_FILENAME
|
||||
|
||||
export interface WorkspaceManifest extends PnpmSettings {
|
||||
packages: string[]
|
||||
|
||||
@@ -28,15 +32,15 @@ export interface WorkspaceManifest extends PnpmSettings {
|
||||
catalogs?: WorkspaceNamedCatalogs
|
||||
}
|
||||
|
||||
export async function readWorkspaceManifest (dir: string): Promise<WorkspaceManifest | undefined> {
|
||||
const manifest = await readManifestRaw(dir)
|
||||
export async function readWorkspaceManifest (dir: string, cfgFileName: ConfigFileName = WORKSPACE_MANIFEST_FILENAME): Promise<WorkspaceManifest | undefined> {
|
||||
const manifest = await readManifestRaw(dir, cfgFileName)
|
||||
validateWorkspaceManifest(manifest)
|
||||
return manifest
|
||||
}
|
||||
|
||||
async function readManifestRaw (dir: string): Promise<unknown> {
|
||||
async function readManifestRaw (dir: string, cfgFileName: ConfigFileName): Promise<unknown> {
|
||||
try {
|
||||
return await readYamlFile.default<WorkspaceManifest>(path.join(dir, WORKSPACE_MANIFEST_FILENAME))
|
||||
return await readYamlFile.default<WorkspaceManifest>(path.join(dir, cfgFileName))
|
||||
} catch (err: unknown) {
|
||||
// File not exists is the same as empty file (undefined)
|
||||
if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
|
||||
|
||||
Reference in New Issue
Block a user