From f5e17d425a6fe2b7cfa06369fe81a189a5cfc3d2 Mon Sep 17 00:00:00 2001 From: Arkadiusz Czekajski Date: Thu, 12 Jun 2025 21:55:02 +0900 Subject: [PATCH 1/5] feat: support optional env var replacements in .npmrc --- docs/lib/content/configuring-npm/npmrc.md | 5 ++++- workspaces/config/lib/env-replace.js | 8 +++++--- workspaces/config/test/env-replace.js | 6 ++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/lib/content/configuring-npm/npmrc.md b/docs/lib/content/configuring-npm/npmrc.md index cd31ae886f132..b0ee60bcb9686 100644 --- a/docs/lib/content/configuring-npm/npmrc.md +++ b/docs/lib/content/configuring-npm/npmrc.md @@ -25,11 +25,14 @@ The four relevant files are: * npm builtin config file (`/path/to/npm/npmrc`) All npm config files are an ini-formatted list of `key = value` parameters. -Environment variables can be replaced using `${VARIABLE_NAME}`. For +Environment variables can be replaced using `${VARIABLE_NAME}`. By default +if the variable is not defined, it is left unreplaced. By adding `?` after +variable name they can be forced to evaluate to an empty string instead.For example: ```bash cache = ${HOME}/.npm-packages +node-options = "${NODE_OPTIONS?} --use-system-ca" ``` Each of these files is loaded, and config options are resolved in priority diff --git a/workspaces/config/lib/env-replace.js b/workspaces/config/lib/env-replace.js index c851f6e4d1501..30dbbeefc45ad 100644 --- a/workspaces/config/lib/env-replace.js +++ b/workspaces/config/lib/env-replace.js @@ -1,9 +1,11 @@ // replace any ${ENV} values with the appropriate environ. +// optional "?" modifier can be used like this: ${ENV?} so in case of the variable being not defined, it evaluates into empty string -const envExpr = /(? f.replace(envExpr, (orig, esc, name) => { - const val = env[name] !== undefined ? env[name] : `$\{${name}}` +module.exports = (f, env) => f.replace(envExpr, (orig, esc, name, modifier) => { + const fallback = modifier === '?' ? '' : `$\{${name}}` + const val = env[name] !== undefined ? env[name] : fallback // consume the escape chars that are relevant. if (esc.length % 2) { diff --git a/workspaces/config/test/env-replace.js b/workspaces/config/test/env-replace.js index c2b570364de87..ed27238b5887f 100644 --- a/workspaces/config/test/env-replace.js +++ b/workspaces/config/test/env-replace.js @@ -8,6 +8,12 @@ const env = { t.equal(envReplace('\\${foo}', env), '${foo}') t.equal(envReplace('\\\\${foo}', env), '\\bar') +t.equal(envReplace('\\\\\\${foo}', env), '\\${foo}') t.equal(envReplace('${baz}', env), '${baz}') t.equal(envReplace('\\${baz}', env), '${baz}') t.equal(envReplace('\\\\${baz}', env), '\\${baz}') +t.equal(envReplace('\\${foo?}', env), '${foo?}') +t.equal(envReplace('\\\\${foo?}', env), '\\bar') +t.equal(envReplace('${baz?}', env), '') +t.equal(envReplace('\\${baz?}', env), '${baz?}') +t.equal(envReplace('\\\\${baz?}', env), '\\') From 8821418d83a674b796589a2f5c60fd1e549546fb Mon Sep 17 00:00:00 2001 From: Arkadiusz Czekajski Date: Wed, 3 Sep 2025 22:43:07 +0900 Subject: [PATCH 2/5] Fix typo in docs Co-authored-by: Michael Smith --- docs/lib/content/configuring-npm/npmrc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/lib/content/configuring-npm/npmrc.md b/docs/lib/content/configuring-npm/npmrc.md index b0ee60bcb9686..47e126f3c3ab0 100644 --- a/docs/lib/content/configuring-npm/npmrc.md +++ b/docs/lib/content/configuring-npm/npmrc.md @@ -27,7 +27,7 @@ The four relevant files are: All npm config files are an ini-formatted list of `key = value` parameters. Environment variables can be replaced using `${VARIABLE_NAME}`. By default if the variable is not defined, it is left unreplaced. By adding `?` after -variable name they can be forced to evaluate to an empty string instead.For +variable name they can be forced to evaluate to an empty string instead. For example: ```bash From 0fa006334b27d52cc03917d287498f92828e15cb Mon Sep 17 00:00:00 2001 From: Arkadiusz Czekajski Date: Wed, 3 Sep 2025 22:43:48 +0900 Subject: [PATCH 3/5] comment styling Co-authored-by: Michael Smith --- workspaces/config/lib/env-replace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/config/lib/env-replace.js b/workspaces/config/lib/env-replace.js index 30dbbeefc45ad..c347be480ed68 100644 --- a/workspaces/config/lib/env-replace.js +++ b/workspaces/config/lib/env-replace.js @@ -1,5 +1,5 @@ // replace any ${ENV} values with the appropriate environ. -// optional "?" modifier can be used like this: ${ENV?} so in case of the variable being not defined, it evaluates into empty string +// optional "?" modifier can be used like this: ${ENV?} so in case of the variable being not defined, it evaluates into empty string. const envExpr = /(? Date: Wed, 3 Sep 2025 22:49:44 +0900 Subject: [PATCH 4/5] improved tests Co-authored-by: Michael Smith --- workspaces/config/test/env-replace.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/workspaces/config/test/env-replace.js b/workspaces/config/test/env-replace.js index ed27238b5887f..a2fb2ae6b0b6e 100644 --- a/workspaces/config/test/env-replace.js +++ b/workspaces/config/test/env-replace.js @@ -6,14 +6,19 @@ const env = { bar: 'baz', } -t.equal(envReplace('\\${foo}', env), '${foo}') -t.equal(envReplace('\\\\${foo}', env), '\\bar') -t.equal(envReplace('\\\\\\${foo}', env), '\\${foo}') -t.equal(envReplace('${baz}', env), '${baz}') -t.equal(envReplace('\\${baz}', env), '${baz}') -t.equal(envReplace('\\\\${baz}', env), '\\${baz}') -t.equal(envReplace('\\${foo?}', env), '${foo?}') -t.equal(envReplace('\\\\${foo?}', env), '\\bar') -t.equal(envReplace('${baz?}', env), '') -t.equal(envReplace('\\${baz?}', env), '${baz?}') -t.equal(envReplace('\\\\${baz?}', env), '\\') +t.equal(envReplace('${foo}', env), 'bar', 'replaces defined variable') +t.equal(envReplace('${foo?}', env), 'bar', 'replaces defined variable with ? modifier') +t.equal(envReplace('${foo}${bar}', env), 'barbaz', 'replaces multiple defined variables') +t.equal(envReplace('${foo?}${baz?}', env), 'bar', 'replaces mixed defined/undefined variables with ? modifier') +t.equal(envReplace('\\${foo}', env), '${foo}', 'escapes normal variable') +t.equal(envReplace('\\\\${foo}', env), '\\bar', 'double escape allows replacement') +t.equal(envReplace('\\\\\\${foo}', env), '\\${foo}', 'triple escape prevents replacement') +t.equal(envReplace('${baz}', env), '${baz}', 'leaves undefined variable unreplaced') +t.equal(envReplace('\\${baz}', env), '${baz}', 'escapes undefined variable') +t.equal(envReplace('\\\\${baz}', env), '\\${baz}', 'double escape with undefined variable') +t.equal(envReplace('\\${foo?}', env), '${foo?}', 'escapes optional variable') +t.equal(envReplace('\\\\${foo?}', env), '\\bar', 'double escape allows optional replacement') +t.equal(envReplace('${baz?}', env), '', 'replaces undefined variable with empty string when using ? modifier') +t.equal(envReplace('\\${baz?}', env), '${baz?}', 'escapes undefined optional variable') +t.equal(envReplace('\\\\${baz?}', env), '\\', 'double escape with undefined optional variable results in empty replacement') + From c51953d8ea9bdda7d022be0e3ae9c0dde8548f37 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 3 Sep 2025 11:40:46 -0700 Subject: [PATCH 5/5] remove extra spaces --- workspaces/config/test/env-replace.js | 1 - 1 file changed, 1 deletion(-) diff --git a/workspaces/config/test/env-replace.js b/workspaces/config/test/env-replace.js index a2fb2ae6b0b6e..c6b40266d30e5 100644 --- a/workspaces/config/test/env-replace.js +++ b/workspaces/config/test/env-replace.js @@ -21,4 +21,3 @@ t.equal(envReplace('\\\\${foo?}', env), '\\bar', 'double escape allows optional t.equal(envReplace('${baz?}', env), '', 'replaces undefined variable with empty string when using ? modifier') t.equal(envReplace('\\${baz?}', env), '${baz?}', 'escapes undefined optional variable') t.equal(envReplace('\\\\${baz?}', env), '\\', 'double escape with undefined optional variable results in empty replacement') -