The prepare hook in npm is your secret weapon for ensuring that code is in its absolutely pristine, ready-to-run state before it ever gets published to the registry. It’s not just about building; it’s about guaranteeing that what you publish is exactly what you tested and what your users will receive.
Let’s see it in action. Imagine you have a small JavaScript library that uses a build step (like TypeScript compilation or Babel transpilation) to produce its final distributable code.
Here’s a simplified package.json:
{
"name": "my-awesome-lib",
"version": "1.0.0",
"description": "A library that does awesome things.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"prepare": "npm run build"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
When you run npm pack or npm publish locally (or when someone else runs npm install on your package if you have prepare configured to run during installation), npm will execute the prepare script before creating the tarball or uploading it. In this case, npm run build will trigger tsc, compiling your TypeScript source files into the dist directory.
The magic of prepare is that it runs automatically in specific scenarios, acting as a gatekeeper for your package’s integrity. It’s invoked when you run npm pack and npm publish. Crucially, it also runs after dependencies have been installed when a package is installed locally from a git repository or a tarball file. This means if you’re developing a package and installing it locally using npm install ../my-awesome-lib, the prepare script will run on that local package, ensuring the build artifacts are present.
This mechanism solves a common problem: accidentally publishing unbuilt code. Without prepare, you might forget to run your build script, publish a package containing only source files, and then have users immediately encounter errors because their environments don’t have the necessary compilers or because the entry points defined in main or module don’t exist. The prepare hook forces the build step to happen, so the dist directory (or whatever your build output is) is populated before the package is finalized and distributed.
The primary lever you control is the scripts.prepare entry in your package.json. You can chain multiple commands here, just like any other npm script. For example, if you need to both compile TypeScript and then copy some static assets:
{
"scripts": {
"build:ts": "tsc",
"copy:assets": "cp src/assets/* dist/",
"prepare": "npm run build:ts && npm run copy:assets"
}
}
This ensures both steps are completed sequentially before publishing.
One aspect that often trips people up is the distinction between prepublish (deprecated) and prepare. While prepublish used to be the hook for this, it was deprecated because it ran in too many unexpected situations, including during npm install from the registry, which could lead to unnecessary and slow builds on every installation. prepare was introduced to replace it, specifically designed to run only before packing and publishing, and during local installs of git/tarball dependencies. This targeted execution prevents the performance issues associated with the older hook.
The most surprising truth about prepare is that it’s also the only hook that runs during npm install for packages installed directly from a git URL or a local tarball. This means if you have a dependency that needs a build step, and you’re installing it using npm install git+ssh://git@github.com:user/repo.git or npm install ./my-local-package.tgz, the prepare script of that dependency will execute, building it in your node_modules directory.
The next concept you’ll likely encounter is managing build artifacts in your .gitignore and .npmignore files.