Node Package Manager (npm) is more than just a tool for installing and managing packages. It provides advanced features that give developers greater control over workflows, dependencies, and security. These features are crucial for building robust and efficient JavaScript applications.
In this topic, you'll explore advanced npm features such as custom npm scripts for task automation, lifecycle hooks for package management, monorepo configurations using npm workspaces, advanced dependency management, and security auditing. Mastering these skills will enhance your development process and make your projects more maintainable.
Custom npm scripts
Custom npm scripts allow you to automate repetitive tasks by defining commands in the scripts section of your package.json. To execute these scripts, use the npm run "script name" command in the terminal. To create a custom script, add it to the scripts section of your package.json file.
Here's an example of how to create and use custom npm scripts:
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"start": "node server.js",
"build": "webpack --mode production",
"test": "jest",
"lint": "eslint .",
"dev": "npm run lint && npm run build && npm start"
}
}In this example, we've defined several custom scripts. To run the script start, you should use the command npm run start. You can combine different custom scripts together: for example, the script dev runs previously defined custom scripts lint, build, and start.
Lifecycle hooks
Besides scripts, npm provides lifecycle hooks, which are predefined scripts that run automatically at specific stages of the package lifecycle, such as during installation or publishing. Common lifecycle hooks are preinstall, postinstall, prepare, and postpublish. These hooks help automate tasks like setting up environments, cleaning up files, or compiling assets.
Here's an example:
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"preinstall": "echo 'Preparing to install dependencies...'",
"postinstall": "node setup.js",
"prepare": "echo 'Preparing to publish npm package...'",
"postpublish": "echo 'Finished publishing npm package...' && rm -rf build"
}
}In the example above, preinstall and postinstall hooks are executed before and after the npm run install command. You can use them when you need to execute additional scripts, move files, remove directories, etc. The prepare and postpublish hooks are executed before and after the npm run publish command when you are publishing your package to the npm registry. For instance, you can remove built files after publishing to reduce space on the hard drive.
The main difference between custom scripts and lifecycle hooks is that custom scripts are run manually (e.g., npm run dev), while lifecycle hooks trigger automatically during specific npm processes. Use custom scripts for project-specific tasks and lifecycle hooks for package-wide setup or cleanup operations.
Monorepo management with npm
A monorepo is a single repository that contains multiple related projects. With npm workspaces, you can manage multiple packages in one repository, sharing dependencies and simplifying the development process.
Let's imagine that we work on a UI component library. Here is the folder structure of the project :
best-ui-library/
package.json
packages/
button/
package.json
index.js
button.css
input/
package.json
index.js
input.cssTo set up a monorepo using npm workspaces, create a package.json file in the root directory and specify the workspace locations.
{
"name": "best-ui-library",
"version": "1.0.0",
"workspaces": [
"packages/*"
]
}This configuration tells npm to look for packages in the packages directory.
Each package within the workspace should have its own package.json file. For example, a package inside the directory button:
{
"name": "@best-ui-library/button",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"package-for-button": "1.0.0"
}
}Benefits of using workspaces in monorepos include:
Shared dependencies across packages: Reduces duplication and ensures consistency.
Simplified version management: Synchronizes updates across all packages.
Easier testing and development of related packages: Facilitates coordinated development and testing.
To run commands for specific workspaces, use the --workspace flag. For example:
npm run test --workspace=@best-ui-library/buttonManaging versions across packages in a monorepo can be done using a tool like Lerna or by manually updating version numbers in each package's package.json file.
Advanced dependency management
Let's briefly recap the dependencies you can install for a Node.js application. To install all dependencies listed, you use the npm install command. You may also have dependencies that are only needed during the development process and are not required for the application to run in production. To save space, you can choose not to install development dependencies by using npm install --production.
To specify a development dependency, you should add the -D or --save-dev flag to the install command, like this: npm i -D devPackageName.
npm offers advanced dependency management features like peer dependencies and optional dependencies. These allow for more flexible and efficient package management in complex projects.
Peer dependencies are used when a package expects another package to be installed at the same level in the dependency tree. This is often used for plugins or add-ons that rely on a core package to function. To define a peer dependency, use the peerDependencies field in your package.json.
For instance, we can improve the package.json from the previous example by adding a component library. Let's say each component relies on React. Instead of including React as a dependency in each component, we can list React as a peer dependency:
{
"name": "@best-ui-library/button",
"version": "1.0.0",
"main": "index.js",
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"dependencies": {
"package-for-button": "1.0.0"
}
}Optional dependencies are not required but provide additional functionality. They're defined in the optionalDependencies field:
{
"name": "@best-ui-library/button",
"version": "1.0.0",
"main": "index.js",
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"dependencies": {
"package-for-button": "1.0.0"
},
"optionalDependencies": {
"additional-ui-theme": "^1.2.3" // not required package
}
}When managing complex dependency trees, it's important to regularly update dependencies and resolve conflicts. You can use commands like npm outdated to check for updates and npm update to update packages.
For more control, you can use npm install packageName@latest to update the package to the latest version, or npm install packageName@version to install a specific version.
In monorepos, npm uses a concept called dependency hoisting. This means that shared dependencies are moved to the root node_modules folder, reducing duplication and saving disk space.
Security auditing
npm provides built-in security features to identify and fix vulnerabilities in project dependencies. To run a security audit, execute npm audit in your project directory. This will produce a report of vulnerabilities, their severity, and recommended actions. The output might look like this:
$ npm audit
=== npm audit security report ===
found 3 vulnerabilities (1 low, 1 moderate, 1 high) in 1234 scanned packages
run `npm audit fix` to fix 2 of them.
1 vulnerability requires manual review. See the full report for details.To automatically fix vulnerabilities, you can run npm audit fix. This command will update your dependencies to patched versions where possible.
You can also configure npm to automatically run security audits with the following command:
npm set audit trueFor vulnerabilities that can't be automatically fixed, you'll need to take manual steps:
Identify the vulnerable package and the specific vulnerability.
Check if there's a newer version of the package that fixes the issue.
If available, update the package manually and test your application.
If no fix is available, consider finding an alternative package or implementing additional security measures in your code.
It's good practice to run npm audit regularly, especially before deploying your application to production. You can also add this to your continuous integration pipeline to catch security issues early in the development process.
More information is available in the official npm documentation.
Conclusion
Advanced npm features provide useful tools for improving your Node.js development workflow.
Custom scripts and lifecycle hooks automate tasks and streamline package management.
Monorepo management with workspaces simplifies handling multiple related projects.
Peer and optional dependencies offer flexibility in package design.
Security auditing keeps your applications safe.
To solidify your understanding, try setting up a project that incorporates these advanced npm features. This hands-on experience will enhance your ability to build maintainable, secure, and efficient JavaScript applications.