Safely Migrating Away From "prepublish" with npm 4

The prepublish script in npm has been contentious for quite some time, and in npm 4 it's finally been deprecated, in the first step on the long road to a real fix. If you're currently using prepublish to ensure certain tasks get run before publish though, actually putting this into action in a development team isn't as easy as it sounds.

The original prepublish script problem was that it not only ran before every npm publish, but also after every npm install. This behaviour was often useful, but frequently surprising, and could be very problematic in some cases, especially if your script was very slow or had side-effects. Here at resin.io we use prepublish scripts in lots of our modules -- like resin-sdk and resin-cli -- typically to guarantee that the compiled output is up to date and their tests are all passing before we publish anything to npm. That works well, but running those on install means it's easy to accidentally have the test suite running twice in CI, both when you install and when you run the tests directly. That can almost double your build time easily, for no benefit.

Npm 4 fixes this, by deprecating prepublish and introducing a new prepublishOnly script, which is only run before a npm publish, and a prepare script, which is run before both npm install and npm publish. Before npm 5, users need to move to prepublishOnly or prepare to get the right behavior they're looking for.

In principle that means if you want to migrate a script that you only want to run before each publish, you can simply change your prepublish for prepublishOnly. This works fine for developers using npm 4, but means any npm < 4 users you have will suddenly not be running any tests or build that you have set up to run before your module is published, which can cause big problems. Npm 4 isn't that widespread yet, and it's very easy to not update your npm version for quite some time, so this creates a big mess if you're not careful.

The solution is to move to prepublishOnly, but add a new prepublish script that ensures that in the cases where it matters (i.e. only when you're publishing, not just installing) you're forced to upgrade to npm 4 or above. That might sound a little tricky, but with a couple of existing command-line Node.js tools and some Bash-fu we can build a fairly simple fix. For example, given the below prepublish script (taken from Resin-SDK):

"scripts": {
    "prepublish": "npm test && npm run build",
}

We can run npm install --save-dev in-publish semver to pull in some useful dependencies, and then change our scripts to:

"scripts": {
    "prepublish":
        "(not-in-publish && echo 'Skipping prepublish') ||
         npm run require-npm4-to-publish",

    "require-npm4-to-publish":
        "semver -r '>=4.0.0' $(npm --version) ||
         (echo 'NPM 4+ required to publish' && exit 1)",

    "prepublishOnly": "npm test && npm run build",
}

(Line-breaks added for readability)

Here, for npm 4 users we now have a prepublishOnly script which does what you expect: before you publish the module, make sure we've rebuilt it and run the tests.

In addition, we've also still got a prepublish script, which checks if you're actually publishing the module (for an install it just prints "Skipping prepublish"), and if so checks you're using a version of npm that matches >=4.0.0. If you are, this is all happy, the prepublishOnly will run as normal. If you're not, this prints a helpful error and exits with a non-zero exit status, blocking the publish entirely.

A little tricky, but easy to put into place once you get your head around it, and a simple working fix to an npm problem that's been annoying Node.js developers for years. Have improvements on this? Add your comments below.

resin_io