How bin linking works in node.js npm and yarn and monorepos
yarn <thing>
pnpm run thing
npm run thing
are types of run commands we run all the time when working in node.js. But, how does that actually all work?
Let's take a quick look at it!
Let's start with PATH
In all the various operating systems, you can set your PATH
environment variable.
The PATH
tells your OS all the places it can look for executables, so you can just run things like node
git
yarn
etc without having to specify the binaries full path on disk. Like c:\program files\some\ridiculously\long\path\git.exe
Feature | macOS (zsh/bash) | Linux (bash/zsh) | Windows (10/11) |
---|---|---|---|
Temporary Change | export PATH=$PATH:/your/path (in Terminal) | export PATH=$PATH:/your/path (in Terminal) | set PATH=%PATH%;C:\your\path (in Command Prompt) |
Permanent (User) | Add export PATH=... to ~/.zshrc or ~/.bash_profile | Add export PATH=... to ~/.bashrc or ~/.profile | Use GUI: System Properties → Environment Variables → Edit Path under User section |
Permanent (System) | Add to /etc/paths or /etc/paths.d/ (requires sudo ) | Add to /etc/environment or /etc/profile (requires sudo ) | Use GUI: Edit Path under System variables (admin rights needed) |
Apply Changes | source ~/.zshrc or restart Terminal | source ~/.bashrc or restart Terminal | Restart Command Prompt or reboot |
Separator | : (colon) | : (colon) | ; (semicolon) |
View Current PATH | echo $PATH | echo $PATH | echo %PATH% (in CMD) or $env:Path (in PowerShell) |
Each OS has a default list of places it looks for binaries. Ubuntu for example has these defaults.
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
On all OS's you can add custom folders to add binary locations to your PATH as well.
On Windows node adds itself to the PATH
during install, which you can see via the settings GUI.

That way you can just run node
in any cmd
or powershell
.

Node version managers such as NVM or NVM For Windows also add path variables to be able to find whichever version of node
you've currently chosen with nvm use vXX.YY
.
In Linux / macOS nvm will add an entry to your Bash or ZSH profile which runs the NVM startup script.

Which does all kinds of calculations, but ultimately ends up updating the PATH
to point to where whatever version of node you're using is located. Usually somewhere in the ~/.nvm
folder.

Let's talk about node_modules and npm
When you install a package with npm install -g
or yarn install --global
, you're installing those packages right next to wherever your current version of node exists in it's node_modules
directory.
See the screenshot above and notice it lives right next to where NVM for Windows has set the node version.
Inside that directory you'll find all the packages that are installed globally.

And all of the "bins"
live right next to node as well..

Those bins more often than not are just shims to call into each node_modules
like ...node node_modules/yarn/bin/yarn.js
Local Packages
Now, for locally installed packages, it's a bit different...
There are .bin
folders are created during npm install
and which are symlinks up and over to the packages which list "bin"
in their package.json
files.

So, for example, the typescript package has a couple of "bin"
entries in it.

When npm install
or yarn
pnmp i
runs, it will look for all those bins, and link them to their corresponding scripts, and on windows it does all kinds of extra files to make sure they're all callable in powershell, cmd, etc.

When using npm run
or yarn run
or even just yarn <bin-name>
you're also able to create your own entries in the "scripts"
and reference the binary name alone like...
{
"scripts": {
"build": "tsc -p tsconfig.web.json"
}
}
Then you can run npm run build
, and it will automatically add all the bins to the path, and allow you to run tsc
./node_modules/.bin
../node_modules/.bin
../../node_modules/.bin
... (up the directory tree)
Similarly, you can invoke the binary directly with npm exec
, since again npm
on the fly modifies the PATH
variable, and adds the node_modules/.bin
to it.


Monorepos
In monorepos using hoisted mode, if you want to define your own "bin"
scripts, you can do so by following the same pattern.
{
"name": "@foo/my-pkg",
"bin": {
"foo-cli": "bin/foo.js"
}
}
You then get symlinks to all the various "bin"
entries created in the root node_modules/.bin
so you can just call yarn foo-cli
from the root of your monorepo.
For example...

Having the ./bin/index.js
will add an entry in the node_modules/.bin
which points to the ./packages/midgard-yarn-strict/bin/index.js

So users of the monorepo can simply run npm exec midgard-yarn-strict
or yarn midgard-yarn-strict
.
All package managers have some form of this behavior including pnpm...

