npm 21 ๐ Local Testing with npm link and npm pack
Testing a package locally before publishing it is not a single workflow. npm link and npm pack solve different problems, and the choice between them determines whether the test reflects the published artifact or a symlinked approximation of it.
Key point: npm link creates a symlink, which means the consumer project sees your working directory directly. npm pack creates a tarball, which means the consumer project installs exactly what npm publish would upload. The symlink is convenient for rapid iteration; the tarball is accurate for validating the package contents.
Why two mechanisms exist
The iteration speed problem. When a package is under active development, rebuilding and reinstalling it into a consumer project on every change is slow. npm link registers the package globally and creates a symlink from the consumer’s node_modules to the package’s directory. Changes to the source are immediately visible without a reinstall, provided the consumer’s bundler or runtime resolves the symlink correctly .
The publication fidelity problem. A symlink does not use the files field, does not apply .npmignore, and does not run the prepack or prepare scripts the way a real publish does. A package can work perfectly when linked and fail when published because a file was excluded by the files allowlist or a build script did not run . npm pack produces the same tarball that npm publish uploads, so installing from the tarball tests the actual artifact.
The peer dependency problem. A linked package is not installed inside the consumer’s node_modules in the normal sense; it is a symlink. This means peer dependencies installed in the consumer cannot be reached from the linked package’s location in the same way they would be from a real installation. A package with a react peer dependency can fail to resolve React when linked into a Next.js project, even though it would work when installed from a tarball .
a. npm link: The Symlink Workflow
The npm link workflow has two steps. In the package directory, npm link creates a global symlink. In the consumer directory, npm link <package-name> creates a symlink from the consumer’s node_modules to the global symlink .
# In the package directory
cd /path/to/my-package
npm link
# In the consumer project
cd /path/to/my-app
npm link my-package
The package name in the consumer command must match the name field in the package’s package.json. For a scoped package, the full scoped name is used: npm link @scope/my-package .
Changes made in the package source are reflected in the consumer immediately, but only if the package does not require a build step. If the package is compiled from TypeScript or bundled, the build must be re-run before the consumer sees the new output . Some bundlers do not follow symlinks by default, which can cause resolution errors in Vite, Nuxt, and Next.js .
To remove the link, the consumer runs npm unlink <package-name> followed by npm install to restore the registry version. The package directory can also run npm unlink to remove the global link .
A subtlety that matters for testing: npm link does not run the preinstall or postinstall hooks, and it does not modify the consumer’s package.json to add the dependency. If the package’s installation depends on those hooks, the link will not reveal it .
b. npm pack: The Tarball Workflow
The npm pack command creates a .tgz file in the package directory. The tarball contains exactly the files that npm publish would upload, after applying the files field, .npmignore, and the always-included files . The consumer installs the tarball with npm install /path/to/package-1.0.0.tgz .
# In the package directory
npm run build
npm pack
# Output: my-package-1.0.0.tgz
# In the consumer project
npm install /path/to/my-package-1.0.0.tgz
The --dry-run flag shows the contents without writing the tarball, which is useful for verifying the files field before committing to a full pack .
The tarball workflow is slower for iteration because every change requires a rebuild, a repack, and a reinstall. But it is the only workflow that validates the package as the registry will receive it. A package that passes the tarball test has passed the files check, the .npmignore check, and the install hooks.
The file: protocol in package.json is a persistent alternative: "my-package": "file:../my-package/my-package-1.0.0.tgz". This is useful when the tarball path should be recorded in the consumer’s dependencies rather than installed ad-hoc .
c. Choosing Between the Two
| Aspect | npm link | npm pack |
|---|---|---|
| Speed of iteration | Immediate | Requires rebuild + repack + reinstall |
Tests files/.npmignore | No | Yes |
| Tests install hooks | No | Yes |
| Works with all bundlers | Not always | Yes |
| Peer dependency resolution | Can fail | Accurate |
Modifies consumer package.json | No | Yes (unless --no-save) |
The practical recommendation is to use npm link during active development and npm pack before every publish. The link is a convenience; the tarball is the validation. If a bundler does not resolve the symlink, the tarball is the only option. If the package has peer dependencies, the tarball is the only accurate test .
A third option, yalc, is a tool that copies the packed tarball into the consumer’s node_modules without a global symlink. It is a compromise between the two: faster than repacking manually, more accurate than a symlink. The local-package-tester package automates the pack-and-install cycle with file watching . For most projects, the native npm link and npm pack are sufficient.
Complete Example Session
# ============================================
# PART 1: BUILD THE PACKAGE
# ============================================
cd /path/to/my-package
npm run build
# ============================================
# PART 2: LINK FOR RAPID ITERATION
# ============================================
npm link
# Creates a global symlink
# ============================================
# PART 3: LINK IN THE CONSUMER
# ============================================
cd /path/to/my-app
npm link my-package
# Creates a symlink in node_modules
# ============================================
# PART 4: ITERATE
# ============================================
# Change source, rebuild, consumer sees changes
cd /path/to/my-package
npm run build
# ============================================
# PART 5: UNLINK AND VERIFY WITH TARBALL
# ============================================
cd /path/to/my-app
npm unlink my-package
npm install
cd /path/to/my-package
npm pack
# Output: my-package-1.0.0.tgz
# ============================================
# PART 6: INSTALL THE TARBALL
# ============================================
cd /path/to/my-app
npm install /path/to/my-package/my-package-1.0.0.tgz
# ============================================
# PART 7: VERIFY THE TARBALL CONTENTS
# ============================================
npm pack --dry-run
# Lists exactly what would be published
# ============================================
# PART 8: CLEAN UP
# ============================================
npm unlink my-package
npm install
The eight parts trace the full workflow: build, link, iterate, unlink, pack, install, verify, clean up. The tarball test is the final gate before npm publish.
Quick Reference
npm link Commands
| Command | Location | Purpose |
|---|---|---|
npm link | Package dir | Create global symlink |
npm link <name> | Consumer dir | Link package into consumer |
npm unlink <name> | Consumer dir | Remove link |
npm ls --link=true | Consumer dir | Show active links |
npm pack Commands
| Command | Purpose |
|---|---|
npm pack | Create tarball |
npm pack --dry-run | List contents without writing |
npm install ./pkg.tgz | Install tarball |
npm install --no-save ./pkg.tgz | Install without modifying package.json |
Workflow Comparison
| Step | npm link | npm pack |
|---|---|---|
| Build | Once | Every change |
| Install into consumer | Symlink | Tarball install |
| Tests publication contents | No | Yes |
| Tests install hooks | No | Yes |
Best Practices
โ Do This:
# Use link for rapid iteration during development
npm link # โ
# Use pack to validate before publishing
npm pack --dry-run # โ
# Clean up links before installing the tarball
npm unlink my-package && npm install # โ
โ Don’t Do This:
# Assume a linked package will publish correctly
# The symlink bypasses the files allowlist and install hooks // โ
# Link a package with peer dependencies and expect accurate resolution
# Peer deps from the consumer are not reachable through the symlink // โ
# Forget to rebuild before the tarball test
# The tarball contains stale build output // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Bundler fails to resolve linked package | Symlinks not followed | Use npm pack or configure the bundler |
| Peer dependency not found | Symlink bypasses consumer’s node_modules | Use npm pack |
| Published package missing files | files/.npmignore not tested with link | npm pack --dry-run |
| Changes not reflected | Package requires rebuild | Run npm run build after changes |
| Link persists after unlink | Consumer package.json still has dependency | npm install to restore |
Real-World Examples
1. Create Global Link
cd /path/to/package && npm link
2. Link into Consumer
cd /path/to/consumer && npm link my-package
3. Verify Link
npm ls --link=true
4. Unlink
npm unlink my-package && npm install
5. Create Tarball
npm pack
6. Install Tarball
npm install /path/to/my-package-1.0.0.tgz
7. Dry-Run Pack
npm pack --dry-run
8. File Protocol in package.json
"my-package": "file:../my-package/my-package-1.0.0.tgz"
9. Scoped Package Link
npm link @scope/my-package
10. Rebuild Before Pack
npm run build && npm pack
Visual
The Link Workflow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PACKAGE DIRECTORY CONSUMER DIRECTORY โ
โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ npm link โ โ npm link โ โ
โ โ โ โ my-package โ โ
โ โโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโ โ
โ โ โ โ
โ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ GLOBAL โโโโโโโโโโโโโโ node_modules/ โ โ
โ โ node_modules โ symlink โ my-package โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Changes in package are visible immediately. โ
โ No reinstall required. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Pack Workflow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PACKAGE DIRECTORY โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ npm run build โ โ
โ โโโโโโโโโโฌโโโโโโโโโ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ npm pack โ Applies files/.npmignore โ
โ โโโโโโโโโโฌโโโโโโโโโ Runs prepack/prepare โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ my-package.tgz โ Exactly what npm publish uploads โ
โ โโโโโโโโโโฌโโโโโโโโโ โ
โ โผ โ
โ CONSUMER DIRECTORY โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ npm install โ โ
โ โ ./my-package.tgzโ Real installation โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Tests publication contents and install hooks. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
When to Use Which
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DURING ACTIVE DEVELOPMENT โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ npm link โ Fast iteration โ
โ โ โ Symlink, no reinstall โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ BEFORE PUBLISHING โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ npm pack โ Accurate validation โ
โ โ โ Tarball, real install โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Link is for speed. Pack is for truth. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
npm link | Creates global symlink + consumer symlink |
npm link <name> | Links package into consumer |
npm unlink <name> | Removes link |
npm pack | Creates tarball of publishable files |
npm pack --dry-run | Lists contents without writing |
npm install ./pkg.tgz | Installs tarball |
| Link bypasses | files, .npmignore, install hooks |
| Pack tests | Publication contents, install hooks |
| Peer deps with link | Often fail |
| Peer deps with pack | Accurate |
Key takeaways:
npm linkis for iteration, not validation. It creates a symlink so that source changes are visible immediately, but it bypasses thefilesallowlist, the.npmignorerules, and the install hooks. A package that works when linked can still fail when published .npm packproduces the exact artifact thatnpm publishuploads. The tarball applies thefilesfield, excludes.npmignorematches, and runs theprepackandpreparescripts. Installing the tarball is the only local test that reflects what consumers will receive .- Peer dependencies are the most common link failure. A linked package is not installed inside the consumer’s
node_modulesin the normal sense, so peer dependencies installed in the consumer cannot be resolved from the linked package’s location. The tarball install does not have this problem . npm pack --dry-runis the verification step. It lists exactly which files will be in the tarball without creating the file. This is the check that catches a missingdist/directory or an accidentally included.envfile before the package is published .- Clean up links before the tarball test. The consumer’s
node_modulesmay still contain the symlink afternpm unlink, and thepackage.jsonmay still reference the linked package.npm unlink <name> && npm installrestores the registry version and ensures the tarball test is isolated.
Remember: The two workflows answer different questions. npm link answers “does the consumer see my changes?” npm pack answers “will the published package work?” The first question is about development speed. The second is about publication correctness. A package that has only been tested with npm link has not been tested for publication. The tarball is the gate.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!