feat(config): allow nodeDownloadMirrors in the global config and env (#13610)
node-download-mirrors was never registered in pnpmTypes. Both the pnpm_config_* environment pass and the global config file allowlist work off that schema, so the setting was invisible to both. nodeDownloadMirrors could only be read from a pnpm-workspace.yaml, which is per-project config, so a Node.js download mirror could not easily be configured once for a machine. This commit registers the key, updates the env parser to accept a JSON object, and adds it to the global config file allowlist. The Rust stack already read this setting from both the environment and the global config file. To keep parity, this commit gives its config_types.rs mirror of pnpmConfigFileKeys the same entry, so the Rust `pnpm config set -g` honours the key. --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
1 parent
bc7dc8bd6e
commit
734cd5bf65
11 files changed
+197
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"pacquet": patch
|
||||
---
|
||||
|
||||
`pnpm config set --global node-download-mirrors` no longer rejects the key. The global config file already accepted `nodeDownloadMirrors`, but the command refused to write it [#13611](https://github.com/pnpm/pnpm/issues/13611).
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@pnpm/config.reader": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
`nodeDownloadMirrors` can now be set in the global config file (`config.yaml`) and through the `PNPM_CONFIG_NODE_DOWNLOAD_MIRRORS` environment variable, so a Node.js download mirror can be configured once for a machine instead of in every workspace [#12124](https://github.com/pnpm/pnpm/issues/12124), [#13611](https://github.com/pnpm/pnpm/issues/13611).
|
||||
|
||||
```sh
|
||||
PNPM_CONFIG_NODE_DOWNLOAD_MIRRORS='{"release":"https://npmmirror.com/mirrors/node/"}'
|
||||
```
|
||||
@@ -276,6 +276,7 @@ const PNPM_CONFIG_FILE_KEYS: &[&str] = &[
|
||||
"minimum-release-age-ignore-missing-time",
|
||||
"minimum-release-age-strict",
|
||||
"network-concurrency",
|
||||
"node-download-mirrors",
|
||||
"node-experimental-package-map",
|
||||
"node-package-map-type",
|
||||
"noproxy",
|
||||
|
||||
@@ -49,6 +49,7 @@ fn config_file_keys() {
|
||||
assert!(is_config_file_key("store-dir"));
|
||||
assert!(is_config_file_key("fetch-timeout"));
|
||||
assert!(is_config_file_key("cache-dir"));
|
||||
assert!(is_config_file_key("node-download-mirrors"));
|
||||
assert!(is_config_file_key("virtual-store-type"));
|
||||
assert!(is_config_file_key("enable-global-virtual-store"));
|
||||
// npm-compatible, not excluded
|
||||
|
||||
@@ -43,6 +43,7 @@ export const pnpmConfigFileKeys = [
|
||||
'minimum-release-age-ignore-missing-time',
|
||||
'minimum-release-age-strict',
|
||||
'network-concurrency',
|
||||
'node-download-mirrors',
|
||||
'node-experimental-package-map',
|
||||
'node-package-map-type',
|
||||
'noproxy',
|
||||
|
||||
@@ -11,6 +11,7 @@ export type ValueConstructor =
|
||||
| ArrayConstructor
|
||||
| BooleanConstructor
|
||||
| NumberConstructor
|
||||
| ObjectConstructor
|
||||
| StringConstructor
|
||||
|
||||
export type ModuleSchema =
|
||||
@@ -127,6 +128,11 @@ function parseValueByConstructor (schema: ValueConstructor, envVar: string): unk
|
||||
return isNaN(value) ? undefined : value
|
||||
}
|
||||
|
||||
if (schema === Object) {
|
||||
const value = tryParseObjectOrArray(envVar)
|
||||
return isStringRecord(value) ? value : undefined
|
||||
}
|
||||
|
||||
if (schema === String) {
|
||||
return envVar
|
||||
}
|
||||
@@ -173,6 +179,12 @@ function tryParseObjectOrArray (envVar: string): object | unknown[] | undefined
|
||||
: result
|
||||
}
|
||||
|
||||
function isStringRecord (value: object | unknown[] | undefined): value is Record<string, string> {
|
||||
return value != null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.values(value).every(item => typeof item === 'string')
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the lowercase suffix if {@link envKey} starts with {@link PREFIX} or
|
||||
* {@link PREFIX_UPPER} and the suffix is fully snake_case (in matching case).
|
||||
|
||||
@@ -102,6 +102,9 @@ export function getOptionsFromPnpmSettings (
|
||||
settings.patchedDependencies[dep] = path.join(manifestDir, patchFile)
|
||||
}
|
||||
}
|
||||
if (pnpmSettings.nodeDownloadMirrors != null) {
|
||||
assertStringRecord(pnpmSettings.nodeDownloadMirrors, 'nodeDownloadMirrors')
|
||||
}
|
||||
translateRegistrySettings(settings)
|
||||
translateUpdateSettings(pnpmSettings, settings)
|
||||
translateAuditSettings(pnpmSettings, settings)
|
||||
@@ -639,6 +642,13 @@ function assertString (value: unknown, settingName: string): asserts value is st
|
||||
}
|
||||
}
|
||||
|
||||
function assertStringRecord (value: unknown, settingName: string): void {
|
||||
assertObjectSetting(value, settingName)
|
||||
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
||||
assertString(item, `${settingName}.${key}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Not an `asserts` guard on purpose: it only rejects malformed shapes at
|
||||
// runtime, without narrowing away the section's declared type at the call site.
|
||||
function assertObjectSetting (value: unknown, settingName: string): void {
|
||||
|
||||
@@ -75,6 +75,7 @@ export const pnpmTypes = {
|
||||
'minimum-release-age-strict': Boolean,
|
||||
'modules-dir': String,
|
||||
'network-concurrency': Number,
|
||||
'node-download-mirrors': Object,
|
||||
'node-experimental-package-map': Boolean,
|
||||
'node-package-map-type': ['standard', 'loose'],
|
||||
'node-linker': ['pnp', 'isolated', 'hoisted'],
|
||||
|
||||
@@ -86,6 +86,37 @@ test('parseEnvVars works with arrays', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('parseEnvVars works with objects', () => {
|
||||
expect(pairsToObject(parseEnvVars(alwaysSchema(Object), {
|
||||
HOME: '/home/fake-user',
|
||||
PATH: '/bin:/usr/bin:/usr/local/bin:/home/fake-user/.bin:/home/fake-user/share/local/bin',
|
||||
pnpm_config_valid_object: '{"release": "https://mirror.example.com/release/"}',
|
||||
pnpm_config_empty_object: '{}',
|
||||
pnpm_config_not_json: 'not an object',
|
||||
pnpm_config_json_array: '["an", "array"]',
|
||||
pnpm_config_json_null: 'null',
|
||||
pnpm_config_json_number: '1',
|
||||
pnpm_config_json_boolean: 'true',
|
||||
pnpm_config_json_string: '"text"',
|
||||
pnpm_config_number_member: '{"release": 42}',
|
||||
pnpm_config_null_member: '{"release": null}',
|
||||
pnpm_config_array_member: '{"release": ["https://mirror.example.com/release/"]}',
|
||||
pnpm_config_undefined_somehow: undefined,
|
||||
}))).toStrictEqual({
|
||||
validObject: { release: 'https://mirror.example.com/release/' },
|
||||
emptyObject: {},
|
||||
notJson: undefined,
|
||||
jsonArray: undefined,
|
||||
jsonNull: undefined,
|
||||
jsonNumber: undefined,
|
||||
jsonBoolean: undefined,
|
||||
jsonString: undefined,
|
||||
numberMember: undefined,
|
||||
nullMember: undefined,
|
||||
arrayMember: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
test('parseEnvVars works with paths', () => {
|
||||
expect(pairsToObject(parseEnvVars(alwaysSchema(path), {
|
||||
HOME: '/home/fake-user',
|
||||
|
||||
@@ -269,6 +269,34 @@ test('getOptionsFromPnpmSettings() rejects non-object overrides values', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
test('getOptionsFromPnpmSettings() accepts nodeDownloadMirrors with string values', () => {
|
||||
expect(() => getOptionsFromPnpmSettings(process.cwd(), {
|
||||
nodeDownloadMirrors: {
|
||||
release: 'https://mirror.example.com/release/',
|
||||
},
|
||||
})).not.toThrow()
|
||||
})
|
||||
|
||||
test('getOptionsFromPnpmSettings() rejects non-string nodeDownloadMirrors values', () => {
|
||||
expect(() => getOptionsFromPnpmSettings(process.cwd(), {
|
||||
nodeDownloadMirrors: {
|
||||
release: 42,
|
||||
} as unknown as Record<string, string>,
|
||||
})).toThrow(expect.objectContaining({
|
||||
code: 'ERR_PNPM_INVALID_SETTING',
|
||||
message: 'The "nodeDownloadMirrors.release" setting should be a string, but got number',
|
||||
}))
|
||||
})
|
||||
|
||||
test('getOptionsFromPnpmSettings() rejects non-object nodeDownloadMirrors', () => {
|
||||
expect(() => getOptionsFromPnpmSettings(process.cwd(), {
|
||||
nodeDownloadMirrors: [] as unknown as Record<string, string>,
|
||||
})).toThrow(expect.objectContaining({
|
||||
code: 'ERR_PNPM_INVALID_SETTING',
|
||||
message: 'The "nodeDownloadMirrors" setting should be an object, but got array',
|
||||
}))
|
||||
})
|
||||
|
||||
test('getOptionsFromPnpmSettings() rejects a non-string range in packageExtensions', () => {
|
||||
expect(() => getOptionsFromPnpmSettings(process.cwd(), {
|
||||
packageExtensions: {
|
||||
|
||||
@@ -4742,6 +4742,51 @@ test.each([
|
||||
expect(config.verifyDepsBeforeRun).toBe(expectedValue)
|
||||
})
|
||||
|
||||
test('loads nodeDownloadMirrors from environment variable pnpm_config_node_download_mirrors', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
writeYamlFileSync('pnpm-workspace.yaml', {
|
||||
nodeDownloadMirrors: {
|
||||
release: 'https://yaml.example.com/release/',
|
||||
},
|
||||
})
|
||||
|
||||
async function getNodeDownloadMirrors (env: NodeJS.ProcessEnv, cliOptions: Record<string, unknown> = {}): Promise<Record<string, string> | undefined> {
|
||||
const { config } = await getConfig({
|
||||
cliOptions,
|
||||
env,
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
return config.nodeDownloadMirrors
|
||||
}
|
||||
|
||||
expect(await getNodeDownloadMirrors({})).toStrictEqual({
|
||||
release: 'https://yaml.example.com/release/',
|
||||
})
|
||||
expect(await getNodeDownloadMirrors({
|
||||
pnpm_config_node_download_mirrors: '{"release":"https://mirror.example.com/release/","rc":"https://mirror.example.com/rc/"}',
|
||||
})).toStrictEqual({
|
||||
release: 'https://mirror.example.com/release/',
|
||||
rc: 'https://mirror.example.com/rc/',
|
||||
})
|
||||
expect(await getNodeDownloadMirrors({
|
||||
PNPM_CONFIG_NODE_DOWNLOAD_MIRRORS: '{"release":"https://upper.example.com/release/"}',
|
||||
})).toStrictEqual({
|
||||
release: 'https://upper.example.com/release/',
|
||||
})
|
||||
expect(await getNodeDownloadMirrors({
|
||||
PNPM_CONFIG_NODE_DOWNLOAD_MIRRORS: '{"release":"https://upper.example.com/release/"}',
|
||||
}, {
|
||||
nodeDownloadMirrors: { release: 'https://cli.example.com/release/' },
|
||||
})).toStrictEqual({
|
||||
release: 'https://cli.example.com/release/',
|
||||
})
|
||||
})
|
||||
|
||||
test('environment variable pnpm_config_* should override pnpm-workspace.yaml', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
@@ -5156,6 +5201,58 @@ describe('global config.yaml', () => {
|
||||
expect(config.scriptShell).toBe('./env-shell.sh')
|
||||
})
|
||||
|
||||
test('reads nodeDownloadMirrors from global config.yaml', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
fs.mkdirSync('.config/pnpm', { recursive: true })
|
||||
writeYamlFileSync('.config/pnpm/config.yaml', {
|
||||
nodeDownloadMirrors: {
|
||||
release: 'https://mirror.example.com/release/',
|
||||
},
|
||||
})
|
||||
|
||||
process.env.XDG_CONFIG_HOME = path.resolve('.config')
|
||||
|
||||
const { config, warnings } = await getConfig({
|
||||
cliOptions: {},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.nodeDownloadMirrors).toStrictEqual({
|
||||
release: 'https://mirror.example.com/release/',
|
||||
})
|
||||
expect(warnings.find((w) => w.includes('global config file'))).toBeUndefined()
|
||||
})
|
||||
|
||||
test('rejects a non-string nodeDownloadMirrors value in global config.yaml', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
fs.mkdirSync('.config/pnpm', { recursive: true })
|
||||
writeYamlFileSync('.config/pnpm/config.yaml', {
|
||||
nodeDownloadMirrors: {
|
||||
release: 42,
|
||||
},
|
||||
})
|
||||
|
||||
process.env.XDG_CONFIG_HOME = path.resolve('.config')
|
||||
|
||||
await expect(getConfig({
|
||||
cliOptions: {},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
workspaceDir: process.cwd(),
|
||||
})).rejects.toThrow(expect.objectContaining({
|
||||
code: 'ERR_PNPM_INVALID_SETTING',
|
||||
message: 'The "nodeDownloadMirrors.release" setting should be a string, but got number',
|
||||
}))
|
||||
})
|
||||
|
||||
test('warns when global config.yaml contains settings that are not allowed in the global config', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
|
||||
Reference in new issue
Block a user