npm postinstall scripts are your secret weapon for setting up packages after they’ve been downloaded.
Let’s see node-sass in action. It’s a prime example of a package that needs a bit of post-install magic. When you npm install node-sass, it doesn’t just drop files into your node_modules. It has a postinstall script defined in its package.json:
{
"name": "node-sass",
"version": "4.13.1",
"scripts": {
"install": "node scripts/install.js",
"postinstall": "node scripts/build.js"
},
// ... other fields
}
When npm install finishes downloading node-sass, it automatically looks for and runs the command specified in the postinstall field: node scripts/build.js. This script is crucial because node-sass is a native addon, meaning it needs to be compiled specifically for your operating system and architecture. The build.js script handles this compilation, downloading pre-built binaries if available or compiling from source if necessary. Without this postinstall step, node-sass wouldn’t work.
This pattern extends to many other packages. You might see postinstall scripts used for:
- Compiling native addons: Like
node-sass,bcrypt,sharp(image processing). These often require C++ compilation or downloading platform-specific binaries. - Setting up development environments: Some packages might run linters, formatters, or generate boilerplate code to get your project ready for development.
- Downloading assets: A package might need to download external data files, dictionaries, or other resources after installation.
- Running database migrations: In some project setups, a
postinstallscript might trigger initial database schema setup. - Generating configuration files: Creating default configuration files based on the project’s needs.
The beauty of postinstall is that it’s automatic. You don’t need to remember to run a separate command. npm handles it for you, ensuring that your dependencies are fully functional right after installation.
Internally, npm maintains a lifecycle for package installation. When you run npm install <package-name>, npm performs several steps:
- Fetch Package: Downloads the package tarball from the registry.
- Extract: Unpacks the tarball into
node_modules/<package-name>. - Run
preinstall(if defined): Executes any script defined in thepreinstallfield of the package’spackage.json. - Run
install(if defined): Executes any script defined in theinstallfield. This is the primary installation script. - Run
postinstall(if defined): Executes any script defined in thepostinstallfield. This is where setup and configuration happen. - Run
prepare(if defined): If the package is published,prepareruns before publishing and also duringnpm install. It’s often used for building code. - Run
prepublish(deprecated, replaced byprepare): Historically used for similar purposes.
The postinstall script receives a specific set of environment variables that can be useful. For instance, npm_package_name will be the name of the package being installed, and npm_package_version will be its version.
Consider a hypothetical package, my-config-generator. Its package.json might look like this:
{
"name": "my-config-generator",
"version": "1.0.0",
"scripts": {
"postinstall": "node ./scripts/setup.js"
},
"dependencies": {
// ...
}
}
And the ./scripts/setup.js file could contain:
// scripts/setup.js
const fs = require('fs');
const path = require('path');
const configPath = path.join(__dirname, '..', 'config', 'default.json');
const defaultConfig = {
apiKey: process.env.MY_API_KEY || 'default_key',
timeout: 5000
};
if (!fs.existsSync(path.join(__dirname, '..', 'config'))) {
fs.mkdirSync(path.join(__dirname, '..', 'config'));
}
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
console.log(`Default config written to ${configPath}`);
When you npm install my-config-generator, this script will automatically run, creating a config directory and a default.json file within the package’s node_modules directory.
It’s important to note that postinstall scripts run every time npm install is executed for that package, including when a project is cloned and npm install is run at the root. They are also executed during npm ci.
The one thing most people miss about postinstall is that it’s executed in the context of the package being installed, not the root project. This means paths within the script, like __dirname in Node.js, refer to the node_modules/<package-name> directory. If your postinstall script needs to interact with the root project’s files or node_modules, you’ll need to use relative paths carefully, often going up several levels (path.join(__dirname, '..', '..', '..')) or leveraging process.cwd().
The next thing you’ll likely encounter is managing prepublishOnly and prepare scripts for publishing packages with build steps.