Skip to content

feat: per-command configs via INI [section] syntax#9223

Draft
reggi wants to merge 1 commit intolatestfrom
reggi/per-command-config
Draft

feat: per-command configs via INI [section] syntax#9223
reggi wants to merge 1 commit intolatestfrom
reggi/per-command-config

Conversation

@reggi
Copy link
Copy Markdown
Contributor

@reggi reggi commented Apr 13, 2026

Overview

Adds support for command-specific configuration in .npmrc files using standard INI section syntax:

registry=http://localhost:1337/npm

[publish]
registry=https://registry.npmjs.org/

Any existing npm config option can be scoped to a specific command by placing it under a [command] section header. This is a framework-level feature — no new per-command flags need to be defined.

CLI support

# Set a registry only for publish
npm config set registry=https://registry.npmjs.org/ --command=publish

# Get the publish-scoped registry value
npm config get registry --command=publish

# Delete a command-scoped value
npm config delete registry --command=publish

Precedence

Command-specific sections always beat non-sectioned values from any file layer, but env vars and CLI flags still win:

default < builtin < global < user < project < [command] < env < cli

Within the command layer, file sources cascade normally (builtin < global < user < project).

Backward compatibility

  • Old npm + new .npmrc: The ini package has always supported [section] syntax. Old npm sees section objects as unknown config keys, emits a warning (Unknown project config "publish"), and ignores them. Sections survive save round-trips via ini.stringify(). No breakage.
  • New npm + old .npmrc: No sections detected, works exactly as before.
  • Prototype pollution: The ini package (v6.0.0) blocks __proto__ sections. setCommandConfig/deleteCommandConfig additionally validate against unsafe keys and use hasOwnProperty() checks.

Changes

  • workspaces/config/lib/index.js — Core: section detection in #loadObject, new command config layer, applyCommand(), setCommandConfig(), deleteCommandConfig(), getCommandConfig()
  • lib/npm.js — Calls config.applyCommand(command) after command name resolution
  • lib/commands/config.js--command flag for set/get/delete
  • workspaces/config/lib/definitions/definitions.js — New command config definition
  • workspaces/config/test/index.js — 10 test cases covering parsing, precedence, save round-trip, set/get/delete
  • docs/lib/content/configuring-npm/npmrc.md — Documentation for [command] sections

Closes #8261

@reggi reggi requested a review from a team as a code owner April 13, 2026 15:45
@reggi reggi marked this pull request as draft April 13, 2026 15:46
if (!hasOwnProperty(conf.raw, commandName)) {
conf.raw[commandName] = {}
}
conf.raw[commandName][key] = val
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk if we pass in the command list to config right now but we should, then we can use that as the de facto list and key off of that

// Also update raw data
const conf = this.data.get(where)
if (hasOwnProperty(conf.raw, commandName)) {
delete conf.raw[commandName][key]
@reggi
Copy link
Copy Markdown
Contributor Author

reggi commented Apr 13, 2026

These wont mesh well with the [trust] command config, meaning trust may not obey these because it parses flags differently and ignores the global config. Ideally we standardize all the old flags into the new way of doing things?

@reggi
Copy link
Copy Markdown
Contributor Author

reggi commented Apr 13, 2026

⚠️ Open Decision: Section name collisions

There are 9 commands whose names collide with existing config keys:

Command Config meaning
access Default access level for publishing
audit Whether to run audit on install
cache Cache directory path
diff Diff options
fund Whether to show fund message
link Link behavior
prefix Global/local prefix path
shrinkwrap Whether to write shrinkwrap
version Version bump behavior

For example, this is ambiguous:

access=restricted

[access]
registry=https://registry.npmjs.org/

Is [access] a config value for the access key, or a command-specific section for npm access?

Proposed solution: command- prefix

Use [command-<name>] syntax to namespace command sections:

access=restricted

[command-access]
registry=https://registry.npmjs.org/

This avoids all collisions today and keeps the door open for other prefixed sections in the future (e.g. hypothetically [registry-<name>] for per-registry config).

Need to decide on this before merging — it affects the section detection logic, --command flag handling, and docs.

@reggi
Copy link
Copy Markdown
Contributor Author

reggi commented Apr 13, 2026

Alternative: Detect object values and treat as command sections regardless

Another option is to keep the unprefixed [access] syntax and handle the collision by type-checking. When ini.parse() encounters a [section] header, it returns the section as a nested object. A regular config value is always a string.

So we could disambiguate at parse time:

// ini.parse() returns:
{
  "access": "restricted",     // string → normal config value
  "publish": {                // object → command section
    "registry": "https://registry.npmjs.org/"
  }
}

If access appears as both a flat key and a section, ini.parse() will return the section object (the section wins since it comes after). But if the user only has [access] with no top-level access= key, the object tells us it is a command section, not a config value. A config value is never an object.

This means the current implementation already handles it — typeof value === "object" is the signal that it is a section, not a config. No prefix needed.

Tradeoff:

  • ✅ Cleaner syntax — [publish] instead of [command-publish]
  • ✅ Already works with current implementation
  • ⚠️ Users cannot set access=restricted globally AND have a [access] command section in the same file (the section would shadow the flat key in ini.parse() output)
  • ❌ Locks us into [name] meaning "command" — no room for future [registry-X] or other prefixed section types without new syntax

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Cannot read properties of null (reading 'edgesOut') - on npm i

3 participants