Local npm Package Testing Made Simple: A Guide to npm pack
Testing npm packages locally before publishing can be tricky, as you will want to verify everything works correctly in a real project before pushing to npm. Let me walk through some common approaches and what has worked best for me.
- npm link: create a symlink between the local package and a project that depends on it.
- npm pack: bundle the package into a .tgz archive to mimic the process of publishing to npm.
- using relative paths in package.json: directly reference a local directory for the dependency.
- npm install with a local directory: install a local package directly without bundling.
- pnpm workspaces: manage multiple packages in a single repository with symlinks.
After trying various approaches, I have settled on a combination of options two and three. While it requires a bit more manual work than other options, I have found it to be more reliable and easier to debug than solutions that rely on linking.
Process#
npm pack
creates a .tgz
tarball file containing files specified in your package’s
files
property. This exactly replicates what users receive when installing from npm —
one of the main reasons I prefer this approach. It respects .gitignore
and .npmignore
files, ensuring development files stay out of the package.
For a project with the following package.json
file:
{
"name": "my-package",
"version": "1.0.0",
"main": "index.js",
"files": [
"build/"
]
}
Running npm pack in this directory creates a file named my-package-1.0.0.tgz
following the
naming convention <package-name>-<version>.tgz
. To preview the included files before
creating the actual tarball, use npm pack --dry-run
:
npm notice 📦 my-package@1.0.0
npm notice === Tarball Contents ===
npm notice 478B package.json
npm notice 12KB build/index.js
npm notice 2.1KB build/index.d.ts
npm notice === Tarball Details ===
npm notice name: my-package
npm notice version: 1.0.0
npm notice filename: my-package-1.0.0.tgz
npm notice package size: 4.8 kB
npm notice unpacked size: 14.5 kB
npm notice shasum: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9
npm notice integrity: sha512-[base64hash]
npm notice total files: 3
This output is helpful for catching any missing or unwanted files before creating the actual package.
This can even be made part of the build process with a script command like
package: vite && npm pack
.
The tarball can then installed in the project using a relative path in
package.json
:
{
"dependencies": {
"my-package": "file:../my-package/my-package-1.0.0.tgz"
}
}
Then run npm install
.
When updating and reinstalling the package, you will likely encounter caching issues — something that caused me considerable confusion initially. npm’s caching behaviour means that if the version in your consuming project’s package.json matches a previously installed version, npm skips fetching the new tarball, even if you’ve updated the file.
I have found two reliable solutions to this:
-
The thorough approach: Bump the version in package.json before each pack operation (e.g.,
"version": "1.0.0-rc1"
). Remember to update the relative file path as well. After installation, checkpackage-lock.json
to verify the correct version. -
The quick approach: Reinstall with cache-busting flags:
npm install ../my-package/my-package-1.0.0.tgz --force --no-cache
While solutions like npm link might seem simpler initially, my experience has shown they often lead to mysterious issues that prove difficult to debug. This tarball-based workflow has become my go-to approach for local package development. The extra manual steps are worth it for the clarity and reliability they provide.