| |

npm 21 ๐ŸŒ Local Testing with npm link and npm pack

โฑ๏ธ Estimated read: 9 min | |

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

Aspectnpm linknpm pack
Speed of iterationImmediateRequires rebuild + repack + reinstall
Tests files/.npmignoreNoYes
Tests install hooksNoYes
Works with all bundlersNot alwaysYes
Peer dependency resolutionCan failAccurate
Modifies consumer package.jsonNoYes (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

CommandLocationPurpose
npm linkPackage dirCreate global symlink
npm link <name>Consumer dirLink package into consumer
npm unlink <name>Consumer dirRemove link
npm ls --link=trueConsumer dirShow active links

npm pack Commands

CommandPurpose
npm packCreate tarball
npm pack --dry-runList contents without writing
npm install ./pkg.tgzInstall tarball
npm install --no-save ./pkg.tgzInstall without modifying package.json

Workflow Comparison

Stepnpm linknpm pack
BuildOnceEvery change
Install into consumerSymlinkTarball install
Tests publication contentsNoYes
Tests install hooksNoYes

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

PitfallWhy It HappensFix
Bundler fails to resolve linked packageSymlinks not followedUse npm pack or configure the bundler
Peer dependency not foundSymlink bypasses consumer’s node_modulesUse npm pack
Published package missing filesfiles/.npmignore not tested with linknpm pack --dry-run
Changes not reflectedPackage requires rebuildRun npm run build after changes
Link persists after unlinkConsumer package.json still has dependencynpm 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

ItemValue
npm linkCreates global symlink + consumer symlink
npm link <name>Links package into consumer
npm unlink <name>Removes link
npm packCreates tarball of publishable files
npm pack --dry-runLists contents without writing
npm install ./pkg.tgzInstalls tarball
Link bypassesfiles, .npmignore, install hooks
Pack testsPublication contents, install hooks
Peer deps with linkOften fail
Peer deps with packAccurate

Key takeaways:

  • npm link is for iteration, not validation. It creates a symlink so that source changes are visible immediately, but it bypasses the files allowlist, the .npmignore rules, and the install hooks. A package that works when linked can still fail when published .
  • npm pack produces the exact artifact that npm publish uploads. The tarball applies the files field, excludes .npmignore matches, and runs the prepack and prepare scripts. 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_modules in 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-run is the verification step. It lists exactly which files will be in the tarball without creating the file. This is the check that catches a missing dist/ directory or an accidentally included .env file before the package is published .
  • Clean up links before the tarball test. The consumer’s node_modules may still contain the symlink after npm unlink, and the package.json may still reference the linked package. npm unlink <name> && npm install restores 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.

Was this helpful?


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!