# Speeding up any and all js-projects with a single, global makefile

> TLDR: [Jump to complete markdown-file](#heading-complete-makefile-for-referance)

When working on various projects, you often need to install dependencies. When switching branches, it's easy to forget this, which can lead to bugs. Consider a branch where one of the dependencies is updated. Each time you switch to and from this branch, you need to reinstall dependencies. Of course, the package manager is often smart enough to install only what is needed, so you usually run the package manager's install command before continuing.

However, running the package manager's install command is still somewhat slow, even if there are no changes. It often also runs some post-install scripts that usually only need to be executed once.

Random example-projects on my computer, all run right after a successful install:

```bash
$ npm install  	1.54s user 0.18s system  45% cpu 3.802 total
$ yarn install 	2.17s user 0.20s system 159% cpu 1.488 total
$ bun install 	0.44s user 0.06s system  18% cpu 2.666 total
```

The example above is not meant to compare different package managers, as they are working on very different projects here. However, there is something much faster than all of these and can be used in any project: *make*.

Make is great in that it is fast to instantiate and can easily be told how to cache resources, thus only performing commands as they are needed:

```bash
make node_modules  0.01s user 0.06s system 159% cpu 0.062 total
```

0.01s is much faster than any other package manager. We can use a Makefile in each project, which I prefer for projects I control. However, it may not always be welcome to add Make, especially if other maintainers are not using it, even though it is great.

We can create a Makefile that can be invoked from any directory, acting on that directory.

With the contents of `~/myscripts/js_project_Makefile`:

```makefile
node_modules: package.json package-lock.json
    npm install
    touch $@
```

For this, I suggest adding this function to your `~/.bashrc`\-file (or `~/.zshrc` etc depending on your shell of choice):

```bash
jsmake() {
  make --no-print-directory -f ~/myscripts/js_project_Makefile -C . node_modules
}
```

Now you can invoke the install-command a lot quicker. However, it does have some drawbacks:

* Only works with npm
    
* Does not run any of the scripts
    

Lets fix this now.

## Works with any package-manager

The first point can be fixed by checking which lock-file exists, since each package-manager uses their own lock-file.

```makefile
define find_lock_file
if [ -f bun.lockb ]; then \
  echo bun.lockb; \
elif [ -f yarn.lock ]; then \
  echo yarn.lock; \
elif [ -f pnpm-lock.yaml ]; then \
  echo pnpm-lock.yaml; \
elif [ -f package-lock.json ]; then \
  echo package-lock.json; \
else \
  echo ""; \
fi
endef

define find_pkg_manager
if [ "$(LOCK_FILE)" = "bun.lockb" ]; then \
	echo bun; \
elif [ "$(LOCK_FILE)" = "yarn.lock" ]; then \
	echo yarn; \
elif [ "$(LOCK_FILE)" = "pnpm-lock.yaml" ]; then \
	echo pnpm; \
else \
	echo npm; \
fi
endef

LOCK_FILE := $(shell $(find_lock_file))
PKG_MNGR := $(shell $(find_pkg_manager))

node_modules: package.json $(LOCK_FILE)
	$(PKG_MNGR) install
	touch $@
```

This allows us to run the correct package manager with `$(PKG_MNGR)`, assuming we can identify the right one to use. While there are likely more package managers available, adding support for additional ones should be straightforward.

Running `jsmake` should now use the correct package manager, so we don't need to remember which one each project uses.

This is great because now I don't need to manually run the package manager's install script. However, I often run the scripts in the `package.json` section. Although I think it's better to use a project makefile for this too, we can create a few helpers to run these scripts with the correct package manager. This has the added benefit of running the install script before the main script, but only if necessary. With this change, I no longer need to remember to call the install script; I just run the script I want, and make will call install if needed.

```makefile
# Runs a script for the package-manager
run: node_modules
    $(PKG_MNGR) run $(SCRIPT_NAME)
```

Make is not really made to take in arguments, so one needs to define key-value pairs. But we can fix this with our `jsmake`\-function like this:

```bash
jsmake() {
    make --no-print-directory -f ~/myscripts/js_project_Makefile -C . run SCRIPT_NAME="$1"
}
```

now we can invoke it sort of like how we are used to:

```bash
$ jsmake dev
```

Since I quite often run a lot of different projects, with different names for all their scripts, I like to have an interactive search for scripts, which will run the selected script. `fzf` is great here, and we can add a tiny integration with adding this to the makefile:

```makefile
# Searches for available scripts interactively (requires fzf), and runs them if found
runSearch: node_modules
	@jq '.scripts | to_entries | map("\(.key)\t\(.value|tostring)")[]' package.json -r | fzf --query="$(SCRIPT_NAME)" --preview 'echo {1}' --select-1 --separator '\t' | cut -d$$'\t' -f1 | xargs -I{} $(PKG_MNGR) run "{}"
```

and then change `jsmake` to:

```bash
jsmake() {
    make --no-print-directory -f ~/myscripts/js_project_Makefile -C . runSearch SCRIPT_NAME="$1"
}
```

Now we have interactive search for scripts, which also runs the install-script as needed.

## Complete makefile for referance:

```makefile
define find_lock_file
if [ -f bun.lockb ]; then \
  echo bun.lockb; \
elif [ -f yarn.lock ]; then \
  echo yarn.lock; \
elif [ -f pnpm-lock.yaml ]; then \
  echo pnpm-lock.yaml; \
elif [ -f package-lock.json ]; then \
  echo package-lock.json; \
else \
  echo ""; \
fi
endef

define find_pkg_manager
if [ "$(LOCK_FILE)" = "bun.lockb" ]; then \
	echo bun; \
elif [ "$(LOCK_FILE)" = "yarn.lock" ]; then \
	echo yarn; \
elif [ "$(LOCK_FILE)" = "pnpm-lock.yaml" ]; then \
	echo pnpm; \
else \
	echo npm; \
fi
endef

LOCK_FILE := $(shell $(find_lock_file))
PKG_MNGR := $(shell $(find_pkg_manager))

node_modules: package.json $(LOCK_FILE)
	$(PKG_MNGR) install
	touch $@
# Runs a script for the package-manager
run: node_modules
	$(PKG_MNGR) run $(SCRIPT_NAME)
# Searches for available scripts interactively (requires fzf), and runs them if found
runSearch: node_modules
	@jq '.scripts | to_entries | map("\(.key)\t\(.value|tostring)")[]' package.json -r | fzf --query="$(SCRIPT_NAME)" --preview 'echo {1}' --select-1 --separator '\t' | cut -d$$'\t' -f1 | xargs -I{} $(PKG_MNGR) run "{}"

.PHONY: run runSearch
```

### Helper for invoking:

```bash
jsmake() {
    make --no-print-directory -f ~/myscripts/js_project_Makefile -C . runSearch SCRIPT_NAME="$1"
}
```
