-.- --. .-. --..

How npm Passes CLI Arguments to modules

When debugging an issue with the node-sass package recently, I ran into a feature of npm install that I used a lot of times, but never thought twice about how it worked. The node-sass package allows you to pass extra command-line arguments like the path to a compiled sass binary path. I never wondered how the args get passed; and more importantly, whether that argument get passed to all the modules that’re getting installed when you run npm install without any package name. Well, until now.

The answer is yes. The argument gets passed to all the modules if naked npm install is run, or just the module if a name is specified. Any module can read the same node-sass-specific command-line flag and work with it if needed.

The way this works internally is after npm cli has processed the common arguments that it requires (think --save-dev etc.), rest of the arguments are then added to the process.env global, each prefixed by npm_config_. So in the following scenario:

# Note: this is not a valid CLI flag for React module. I'm just using it to demonstrate
# the behaviour

npm install react --dest-dir=src

When the install life-cycle event of the module gets run1, a variable process.env.npm_config_dest_dir can be used to get at the value src. Incidentally, this is the same object that gets populated if you have a project/global config in npm config too! So you can do this, instead:

# Note: this is not a valid CLI flag for React module. I'm just using it to demonstrate
# the behaviour
cat .npmrc
dest-dir=src

npm install react

The third alternative that works in the same way is when you use an environmental variable (case-insensitive!) that has NPM_CONFIG as the prefix. So the above two variants are the same as the following:

# Note: this is not a valid CLI flag for React module. I'm just using it to demonstrate
# the behaviour

NPM_CONFIG_DEST_DIR=src npm install react

Notice that in these examples, the hyphenated arguments are auto-converted to underscore versions when they get passed via the env.


The code flow

This entire processing happens at two different stages: first, the arguments and configs are processed, and the second part is where this information is re-injected into the proccess.env.npm_config_ prefixed list.

The first part is achieved by npm using the config-chain module. The location for local and global npmrc, and the command line flags that were passed to the npm command itself among others are passed to this module to create a config object. This is the place where any env var that has the NPM_CONFIG prefix gets addded to the config list as a parameter, with the prefix stripped out.

The second part happens, from the looks of it, only inside the npm life-cycle events. These events are invoked by calling into a separate module that was extracted out from npm/cli, and handles all the necessary work that needs to be done when running the life-cycle events, and this is where the config data built in the previous step gets added to process.env.<prefix>.


Footnotes

  1. npm defaults to running the following commands for common features like npm install, which can be overridden. For example, the node-sass package overrides this to run a script instead: scripts/install.js