npm install if package.json was modified
Asked Answered
S

6

35

TL;DR: Is there a way to have npm install run automatically before running any npm script if your package.json has been modified?

Problem Scenario

You pull or checkout a branch that updated package.json. You run npm run my-script. my-script depends on a package that has newly been added to package.json. my-script fails. You wonder why. Before flipping over your desk you run npm install just to be sure. my-script runs successfully. You don't need a new desk.

I know that build / task runner tools like gradle make sure that your dependencies are up-to-date before running a task. I has always been a (minor) pain point that npm doesn't do it. I stumbled over two solutions that I don't particluarly like.

Non-Ideal Solution: make

Instead of relying on npm scripts in your package.json to run commands you use make and make use of its integrated dependency tracking with the following trick:

# Smart install: Only executes if package.json's
# modification date is later than node_module's

node_modules: package.json
    npm install
    @rm -f node_modules/.modified
    @touch -m node_modules/.modified

install: node_modules 

Source: https://mattandre.ws/2016/05/make-for-hipsters/

The problem is that you know have to rely on make to run scripts and lose certain advantages of npm scripts such as conveniently referring to other scripts and running scripts in parallel (npm-run-all). It's also harder to work with others if they don't know make or have problems running it (Windows). It's an archaic tool outside of the node/npm ecosystem and too costly just for this smart install advantage.

Non-Ideal Solution: Git hook

Another way is to add a post-merge git hook.

The problem is that this solution is local to the repository and can't be easily shared. npm install will only be run automatically on git merges. When you change package.json in any other way you still have to remember running npm install. Admittedly, that's a minor point in practice. Nonetheless, it would be nice to never have to think about running npm install at all when you want to run a script.

Source: https://davidwalsh.name/git-hook-npm-install-package-json-modified

Ideal Solution

I'd like to define my package.json in a way similar to:

{
  "scripts": {
    "pre-run": "npm-smart-install",
    "my-script": "…"
  },
  "dependencies": {
    "npm-smart-install": "1.0.0"
  }
}

npm-smart-install is a hypothetical npm package that I wish existed. pre-run is a hypothetical npm-scripts lifecycle hook. When I run npm run my-script and package.json has been modified since the last run of any script, run npm install before running my-script.

To repeat: Is there a way to have npm install run automatically before running any npm script if your package.json has been modified without relying on tools outside the npm ecosystem?

Skyrocket answered 23/9, 2018 at 13:46 Comment(7)
Your hypothetical package might not be very hard to implement yourself. You could generate a hash digest of package.json upon the first install and then use it in subsequent installs to check if the file changed. This looks like it does something similar.Darned
Also, why not just run npm install before any script? If everything is there with the correct version then it won't do anything.Darned
This would delay the running of each script.Skyrocket
This sounds like a problem NPM's tink is hoping to solve (albeit that's years away).Demurral
Maybe we should make a "npmi-run-all" that does this. We could store the hash in the tmpDir provided by fsSpangle
I'm almost done with the package. Trying to figure out how I can execute it like in your ideal solution scenario.Darned
Here's corrected link to NPM's tink blog postRoseline
D
8

Okay so I'm done with the package. Here it is. You can use it exactly the same way you specified in your ideal scenario. Just npm install install-changed and add it to a custom script, like pre-run in your example. It should figure out whether or not it needs to npm install and does so if it needs to.

 {
  "scripts": {
    "pre-run": "install-changed",
    "my-script": "…"
  },

You can also do this programatically but I don't think you're going to need this.

let installChanged = require('install-changed')

let isModified = installChanged.watchPackage() 

The function above does exactly the same thing, additonally it also returns a boolean value which you might find useful.

Darned answered 23/9, 2018 at 18:13 Comment(4)
Good work! I just wish npm had an integrated 'prerun' hook. As it is now, I'd have to either write "my-script": "npm run prerun && …" or "premy-script": "install-changed", "my-script": "…".Skyrocket
instead of packagehash.txt it should use the current version of package.json from the git historyCompurgation
There's a fork of install-changed called package-changed, it's basically the same thing but is more flexible, allows you to run any command when package.json deps/devDeps change. I then forked this fork and named it lockfile-changed, since it instead just creates hash of yarn.lock contents. It is all about yarn. I recommend using package-changed unless you really want it to be based on a yarn.lock+use yarn. If you use my package, it might have bugs. I haven't tested it quite yet.Roseline
@Darned Great, it worked for my project, but unfortunately, i am getting an error when I execute the same with the pipeline "pre-run: Failed to exec pre-run script"Cherellecheremis
M
6

You can create a custom script that will run your smart install.

smart-install.sh file

#!/usr/bin/env bash

changedFiles="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"

checkForChangedFiles() {
    echo "$changedFiles" | grep --quiet "$1" && eval "$2"
}

packageJsonHasChanged() {
  echo "Changes to package.json detected, installing updates"
  npm i
}

checkForChangedFiles package.json packageJsonHasChanged

Then if you have husky you can add that to the post-checkout hook or any hook you like. If you don't have husky, you can also add it directly to the scripts which essentially do the same thing.

.huskyrc file

{
  "hooks": {
    "post-checkout": "npm run smart-install"
  }
}

package.json file

"scripts": {
  ...
  "smart-install": "bash ./bin/smart-install.sh",
}

Either way it's a good idea to create a npm script to run smart-install

Millrace answered 2/6, 2020 at 16:10 Comment(1)
similar to yarnhook: github.com/frontsideair/yarnhookRoseline
R
5

You can also do this using husky post-checkout hook by adding this piece of code there.

if [ $(git diff HEAD@{1}..HEAD@{0} -- "package.json" | wc -l) -gt 0 ];
then
  npm ci
fi
Requisition answered 30/11, 2022 at 2:32 Comment(0)
S
3

One easy solution is to use git diff and grep

git diff --name-only HEAD HEAD~1 | grep package.json && npm install

this command first check difference between latest commit and last commit, from which it will list the files that are changed in latest.After that grep is used to find "package.json" in that list.If it finds that in the list it will run "npm install" either it won't.

Stalemate answered 21/6, 2022 at 5:21 Comment(1)
how can we add this step to the automatic build?Cherellecheremis
F
1

Like other answers, but I think simpler because it's one line of shell script in package.json:

{
    "scripts": {
        "install-if-needed": "[ package.json -nt node_modules ] && npm install && touch node_modules",
        "my-script": "npm run install-if-needed && ..."
    }
}

or, basically equivalent:

{
    "scripts": {
        "install-if-needed": "[ package.json -nt node_modules ] && npm install && touch node_modules",
        "premy-script": "npm run install-if-needed",
        "my-script": "..."
    }
}

You'll have to either inline npm run install-if-needed or have a pre... script for each script that needs it -- I don't know any other way to have it run before multiple other scripts.

Explanation: install-if-needed checks the modification times on package.json and node_modules. If node_modules is newer, it does nothing; otherwise it runs npm install. The final touch node_modules is necessary because npm install may itself change the package.json modification time (if it is correcting whitespace in package.json for example).

Floe answered 30/11, 2020 at 23:45 Comment(1)
Last modified time of node_modules is not going to be as reliable as a hash of npm/yarn lockfile :/Roseline
I
1

If you want to install NPM packages after a change of git branch, you can do this with a git hook.

  1. Create the hook (script):

    nano .git/hooks/post-checkout
    
#!/bin/bash                                                                      
                                                                                     
set -e                                                                           
                                                                                     
printf '\npost-checkout hook\n\n'                                                
                                                                                     
prevHEAD=$1                                                                      
newHEAD=$2                                                                       
checkoutType=$3                                                                  
                                                                                     
[[ $checkoutType == 1 ]] && checkoutType='branch' ||                             
                            checkoutType='file' ;                                
                                                                                    
echo 'Checkout type: '$checkoutType                                              
echo '    prev HEAD: '`git name-rev --name-only $prevHEAD`                       
echo '     new HEAD: '`git name-rev --name-only $newHEAD`

printf '\n'
echo 'Installing packages of package*.json of branch '`git name-rev --name-only $newHEAD`
printf '\n\n'

npm ci
  1. Make it executable
chmod u+x .git/hooks/post-checkout

Now everytime you change branch with git checkout or git switch the packages of the new branch are installed according to your package.json and package-lock.json

You can also create other hooks, for example post-merge which is run after a git pull.

Interminable answered 6/6 at 20:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.