Achieve “npm run x” behavior without a “scripts” entry?

To run a node command within the "context" of your installed node_modules, you can make an entry in the scripts field of package.json. Eg:

...
  "scripts": {
    "test": "mocha --recursive test/**/*.js --compilers js:babel-register"
  }
...

and then I can type npm run test in my project root, and the mocha tests will run (by calling the mocha binary installed in node_modules/mocha/bin).

Is there a way to achieve precisely the same behavior but without creating a scripts entry? Eg, for a one-off "script"?

I'm imagining something like the following, as an equivalent to npm run test:

npm cmd mocha --recursive test/**/*.js --compilers js:babel-register

Is there any way to achieve this?

NOTE: I should clarify that I'm looking for true equivalence. That is, my command should be able to access other script commands, etc. I know you can always call the binaries using node and the path to the binary within node_modules, but that's not an adequate solution.

Answers:

Answer

Note: This answer addresses the OP's specific use case: calling the CLIs of dependent packages in the context of a given project; it is not about making CLIs globally available - see bottom for a discussion.

tl;dr:

On Unix-like platforms, prepend npm run env -- to your command; e.g.:

npm run env -- mocha --recursive test/**/*.js --compilers js:babel-register

This not only enables calling of dependent CLIs by mere name, but fully replicates the environment that npm sets behind the scenes when you use npm test or npm run-script <script-defined-in-package.json>.

Sadly, this approach doesn't work on Windows.

For Windows solutions, convenience aliases (including once-per-session environment-configuration commands), and background information, read on.


There are two (not mutually exclusive) approaches to making an npm project's dependencies' CLIs callable by mere name from the shell:

  • (a) Use a per-invocation helper command that you pass your commands to.
  • (b) Run a once-per-session command that (temporarily) modifies your environment.

Frxstrem's helpful answer provides an incomplete solution for (a) on Unix-like platforms; it may, however, be sufficient, depending on your specific needs.
It is incomplete in that it merely prepends the directory containing (symlinks to) the dependent CLIs to the $PATH, without performing all other environment modifications that happen when you invoke npm test or npm run-script <script-defined-in-package.json.


Unix convenience and Windows solutions

Note that all solutions below are based on npm run env, which ensures that all necessary environment variables are set, just as they are when your run scripts predefined in the project's package.json file with npm test or npm run-script <script>.
These environment modifications include:

  • Prepending $(npm prefix -g)/node_modules/npm/bin/node-gyp-bin and the project directory's ./node_modules/.bin subdirectory, which is where symlinks to the dependencies' CLIs are located, (temporarily) to the $PATH environment variable.
  • Defining numerous npm_* environment variables that reflect the project's settings, such as npm_package_version as well as the npm / node environment.

Convenience solutions for Unix-like platforms:

Both solutions below are alias-based, which in the case of (a) is a more light-weight alternative to using a script, and in the case of (b) is a prerequisite to allow modification of the current shell's environment (although a shell function could be used too).

For convenience, add these aliases to your shell profile/initialization file.

(a) Per-invocation helper:

Defining

alias nx='npm run-script env --'

allows you to invoke your commands ad-hoc simply by prepending nx; e.g.:

nx mocha --recursive test/**/*.js --compilers js:babel-register

(b) Once-per-session configuration command:

alias npmenv='npm run env -- $SHELL'

Run npmenv to enter a child shell with the the npm environment set, allowing direct (by-name-only) invocation of dependent CLIs in that child shell.
In other words, use this as follows:

cd ~/some-npm-project
npmenv # after this, you can run dependent CLIs by name alone; e.g., `mocha ...`
# ... run your project-specific commands
exit  # exit the child shell before you switch to a different project

Windows solutions:

(a) and (b): Note that Windows (unlike POSIX-like shells on Unix-like platforms) doesn't (directly) support passing environment variables scoped to a single command only, so the commands below, even when passed a specific command to execute (case (a)), invariably also modify the session's environment (case (b)).

PowerShell (also works in the Unix versions):

Add the following function to your $PROFILE (user-specific profile script):

function npmenv($commandIfAny) {
  npm run env -- |
    ? { $_ -and $_ -notmatch '^>' -and $_ -match '^[a-z_][a-z0-9_]+=' } | 
      % { $name, $val = $_ -split '='; set-item -path "env:$name" -value $val }
  if ($?) {
    if ($commandIfAny) {
      & $commandIfAny $Args
    }
  } 
}

cmd.exe (regular command prompt, often mistakenly called the "DOS prompt"):

Create a batch file named npmenv.cmd, place it in a folder in your %PATH%, and define it as follows:

@echo off
:: Set all environment variables that `npm run env` reports.
for /f "delims==; tokens=1,*" %%i in ('npm run env ^| findstr /v "^>"') do set "%%i=%%j"
:: Invoke a specified command, if any.
%*

Usage (both cmd.exe and PowerShell):

For use case (b), invoke simply as npmenv without arguments; after that, you can call dependent CLIs by mere name (mocha ...).

For use case (a), prepend npmenv to your command; e.g.:

 npmenv mocha --recursive test/**/*.js --compilers js:babel-register

Caveat: As noted, the first invocation of npmenv - whether with or without arguments - invariably modifies the %PATH% / $env:PATH variable for the remainder of the session.

If you switch to a different project in the same session, be sure to run npmenv (at least once) again, but note that this prepends additional directories to %PATH%, so you you could still end up accidentally running a previous project's executable if it isn't an installed dependency of the now-current project.

From PowerShell, you could actually combine the two solutions to get distinct (a) and (b) functionality after all: define the *.cmd file above as a distinct command (using a different name such as nx.cmd) that you only use with arguments (a), and redefine the PowerShell function to serve as the argument-less environment modification-only complement (b).
This works, because PowerShell invariably runs *.cmd files in a child process that cannot affect the current PowerShell session's environment.


Notes on the scope of the question and this answer:

The OP's question is about calling the CLIs of already-installed dependent packages in the context of a given project ad-hoc, by mere executable name - just as npm allows you to do in the commands added to the scripts key in the package.json file.

The question is not about making CLIs globally available (by installing them with npm install -g).

In fact, if you want to author modular, self-contained packages, do not depend on globally installed packages. Instead, make all dependent packages part of your project: use npm install --save (for runtime dependencies) and npm install --save-dev (for development time-only dependencies) - see https://docs.npmjs.com/cli/install

In particular, if a given CLI is already installed as a dependency, installing it globally as well (potentially a different version) is not only redundant, but asking for confusion over which version is executed when.

Answer

I've saved this little script as ~/bin/npm-cmd on my computer:

#!/bin/bash
PATH="$(npm bin):$PATH" "[email protected]"

Then running npm-cmd PROGRAMARGS should look for PROGRAM in ./node_modules/.bin first, before falling back to normal lookup.

Answer

Note: This answer works on all platforms.

npm i -g mocha

mocha --recursive test/**/*.js --compilers js:babel-register

Mocha CLI documentation (scroll to usage section).


If you want to run an npm package as an executable from the CLI, the right way to do it is via the package.json bin entry. It allows you to map files in your project as executables. The first line of the files, like normal scripts should start with a shebang, #!/usr/bin/env node if it's a node script.

By mapping these files in your package.json bin, you will be able to call them from other packages scripts section, or call them from the cli when your package is installed globally with npm install -g <package>. The bin node can be an object that maps script names to script files within your project if you want to add multiple scripts to your path as executables, or a string path of a single file that you would like to act as the executable for your package.

This is in fact the exact same mechanism used by mocha in your example:

mocha bin

Without this, mocha would never be in your path when installing it globally, and would not be accessible via your scripts section.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.