[BUG] Impossible to reliably passthrough NODE_OPTIONS in .npmrc
Is there an existing issue for this?
- [x] I have searched the existing issues
This issue exists in the latest npm version
- [x] I am using the latest npm
Current Behavior
If I add following line to .npmrc:
node-options = "${NODE_OPTIONS} --use-system-ca"
it correctly works if the NODE_OPTIONS was set before to some node flags in my env. For example, if I do
export NODE_OPTIONS=--inspect
npm run foo
then the Node being run by the "foo" script, receives --inspect --use-system-ca as node options and it's fine.
But, if the NODE_OPTIONS env var is not set to anything when i do npm run foo, then the Node receives ${NODE_OPTIONS} --use-system-ca as options and Node rejects all the options (--use-system-ca is ignored as well).
As a result of npm leaving var substitutions untouched if the var is not set, there is no way to reliably extend the node options via .npmrc instead of replacing them.
Expected Behavior
Either:
${FOO}var substitution pattern should eval to empty string if FOO is not defined- or possibility to provide defaults in a bash-like way should be possible. So
${FOO:-''}can be used to just have an empty string if the var is not defined at all
Steps To Reproduce
- Add this script to package.json:
"foo": "node -e 'console.log(process.env.NODE_OPTIONS);'" - Add this line in .npmrc:
node-options = "${NODE_OPTIONS} --max-old-space-size=1" - run
NODE_OPTIONS='' npm run foo- as expected the Node will fail to run because it runs out of the assigned 1mb of memory, meaning the flags were passed just fine unset NODE_OPTIONSto make sure it is not set to any value, not even an empty stringnpm run foo- You can see
${NODE_OPTIONS} --max-old-space-size=1printed and Node finishes just fine which means that the NODE_OPTIONS were ignored because of the incorrect string resulting from unsubstituted variable
Environment
- npm: 11.4.1
- Node.js: 22.15.1
- OS Name: Windows
- System Model Name:
- npm config:
; "user" config from C:\Users\(...)\.npmrc
email = (protected)
script-shell = "C:\\Program Files\\Git\\usr\\bin\\bash.exe"
; "project" config from C:\(...)\.npmrc
fetch-retries = 4
legacy-peer-deps = true
node-options = "${NODE_OPTIONS} --max-old-space-size=1"
save-exact = true
save-prefix = ""
; node bin location = C:\Users\(...)\AppData\Local\fnm_multishells\32040_1748498818910\node.exe
; node version = v22.15.1
; npm local prefix = C:\(...)
; npm version = 10.9.2
; cwd = C:\(...)
; HOME = C:\Users\(...)
Note to maintainers: this is the same behavior in npm 10.8.2
EDIT: I do agree with the author of the issue
I am writing to express my interest in addressing the issue involving environment variable substitution in .npmrc—specifically, how ${NODE_OPTIONS} behaves when the variable is undefined. This behavior currently causes Node.js to fail or ignore critical flags due to incorrect string substitution, which leads to unreliable configurations for developers. The Problem When node-options = "${NODE_OPTIONS} --max-old-space-size=1" is used in .npmrc, it works as expected only if NODE_OPTIONS is already set. However, if it's not defined at all, the placeholder is passed literally as ${NODE_OPTIONS}, causing Node.js to reject or ignore all flags. This behavior breaks consistency and creates potential runtime issues.
The entire replacement boils down to https://github.com/npm/cli/blob/latest/workspaces/config/lib/env-replace.js file which has a simple code for replacing env vars after ini package parses the config.
I reckoned that existing projects might rely on the variables not being replaced if the value is not defined so silently changing the code to evaluate to empty string if the env var is undefined might not be a good idea.
Then I considered what would it take to implement ${FOO:default} syntax and quickly relized that it falls into a parsing and escaping hell (what if someone wants to default to string that contains } or \\ or \\} etc). Having defaults would be nice but solving the original problem doesn't need it.
So after all, since the syntax is custom anyway, I though that simple and reliable solution would be to introduce a ? modifier. It could be put after the var name, like this: ${FOO?} and signals that the var is optional and can be evaluated to empty string.
Summary of the idea:
- when
FOOis undefined${FOO}evals to${FOO}(unchanged behavior)${FOO?}evals to empty string (new behavior)
- when
FOOis set tobar${FOO}evals tobar(unchanged behavior)${FOO?}evals tobar(unchanged behavior)
try updating the env-replace.js logic to support optional environment variables using the ${VAR?} syntax. This means:
${FOO} works the same as before.
${FOO?} now safely evaluates to an empty string if FOO is undefined.
This avoids breaking existing behavior while offering more flexibility. It also handles escaped expressions like ${FOO} correctly.
On Fri, May 30, 2025 at 1:33 PM Arkadiusz Czekajski < @.***> wrote:
aczekajski left a comment (npm/cli#8335) https://github.com/npm/cli/issues/8335#issuecomment-2922275359
The entire replacement boils down to https://github.com/npm/cli/blob/latest/workspaces/config/lib/env-replace.js file which has a simple code for replacing env vars after ini package parses the config.
I reckoned that existing projects might rely on the variables not being replaced if the value is not defined so silently changing the code to evaluate to empty string if the env var is undefined might not be a good idea.
Then I considered what would it take to implement ${FOO:default} syntax and quickly relized that it falls into a parsing and escaping hell (what if someone wants to default to string that contains } or \ or \} etc). HAving defaults would be nice but solving the original problem doesn't need it.
So after all, since the syntax is custom anyway, I though that simple and reliable solution would be to introduce a ? modifier. It could be put after the var name, like this: ${FOO?} and signals that the var is optional and can be evaluated to empty string.
Summary of the idea:
- when FOO is undefined
- ${FOO} evals to ${FOO} (unchanged behavior)
- ${FOO?} evals to empty string (new behavior)
- when FOO is set to bar
- ${FOO} evals to bar (unchanged behavior)
- ${FOO?} evals to bar (unchanged behavior)
— Reply to this email directly, view it on GitHub https://github.com/npm/cli/issues/8335#issuecomment-2922275359, or unsubscribe https://github.com/notifications/unsubscribe-auth/A4Y2NHXKG6H4XCSKJJ7XAFT3BBFYBAVCNFSM6AAAAAB6EWCUAGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDSMRSGI3TKMZVHE . You are receiving this because you commented.Message ID: @.***>
Existuje pro to nějaký existující problém?
- [x] Prohledal jsem existující problémy
Tento problém existuje v nejnovější verzi npm.
- [x] Používám nejnovější npm
Aktuální chování
Pokud přidám následující řádek do
.npmrc:node-options = "${NODE_OPTIONS} --use-system-ca"Funguje to správně, pokud byl NODE_OPTIONS předtím nastaven na některé příznaky uzlů v mém prostředí. Například, pokud to udělám
export NODE_OPTIONS=--inspect npm run fooPak uzel spuštěný skriptem "foo" přijímá
--inspect --use-system-cajako volby uzlu a je to v pořádku.Pokud ale
NODE_OPTIONSproměnná prostředí (env var) není v danou chvíli nastavena na žádnou hodnotunpm run foo, uzel je přijme${NODE_OPTIONS} --use-system-cajako možnosti a uzel je všechny možnosti odmítne (--use-system-cataké je ignorován).V důsledku toho, že npm ponechává substituce proměnných nedotčené, pokud proměnná není nastavena, neexistuje způsob, jak spolehlivě rozšířit možnosti uzlu pomocí .npmrc namísto jejich nahrazení.
Očekávané chování
Buď:
${FOO}Vzor substituce var by měl být eval na prázdný řetězec, pokud není definován FOO.- Nebo by měla být možná možnost zadat výchozí hodnoty způsobem podobným bash.
${FOO:-''}Lze tedy použít pouze prázdný řetězec, pokud proměnná není vůbec definována.Kroky k reprodukci
- Přidejte tento skript do souboru package.json:
"foo": "node -e 'console.log(process.env.NODE_OPTIONS);'"- Přidejte tento řádek do souboru .npmrc:
node-options = "${NODE_OPTIONS} --max-old-space-size=1"- spustit
NODE_OPTIONS='' npm run foo- jak se očekávalo, uzel se nespustí, protože mu dojde přidělený 1 MB paměti, což znamená, že příznaky byly předány v pořádkuunset NODE_OPTIONSaby se ujistil, že není nastaven na žádnou hodnotu, ani na prázdný řetězecnpm run foo- Vidíte
${NODE_OPTIONS} --max-old-space-size=1vytištěné a Node dokončí v pořádku, což znamená, že NODE_OPTIONS byly ignorovány kvůli nesprávnému řetězci vyplývajícímu z nesubstituované proměnné.Prostředí
- npm: 11.4.1
- Node.js: 22.15.1
- Název operačního systému: Windows
- Název modelu systému:
- konfigurace npm:
; "user" config from C:\Users(...).npmrc
email = (protected) script-shell = "C:\Program Files\Git\usr\bin\bash.exe"
; "project" config from C:(...).npmrc
fetch-retries = 4 legacy-peer-deps = true node-options = "${NODE_OPTIONS} --max-old-space-size=1" save-exact = true save-prefix = ""
; node bin location = C:\Users(...)\AppData\Local\fnm_multishells\32040_1748498818910\node.exe ; node version = v22.15.1 ; npm local prefix = C:(...) ; npm version = 10.9.2 ; cwd = C:(...) ; HOME = C:\Users(...)