NPM Link and Gulp: Understanding the Power of Local Development

npm link creates a symlink between a globally-linked local package and another project, so you can develop a package and test it live in a consuming project without publishing it to the registry first.


In this post: Worked example: building a custom Gulp plugin · The gotcha to know about · Confirming what actually got linked · Unlinking and other package managers · npm workspaces: try this first · Alternatives · When would you actually use this? · Related reading


Worked example: building a custom Gulp plugin

Gulp plugins are usually developed as standalone packages — npm link lets you build and test one in a real project before publishing it.

mkdir gulp-myplugin && cd gulp-myplugin
npm init -y
npm install gulp --save-dev

index.js:

const through = require('through2');

function myPlugin() {
    return through.obj((file, enc, cb) => {
        if (file.isBuffer()) {
            let contents = file.contents.toString(enc);
            file.contents = Buffer.from(contents.toUpperCase());
        }
        cb(null, file);
    });
}

module.exports = myPlugin;

Link it, then use it from the other project:

npm link

cd ../my-gulp-project
npm link gulp-myplugin

Then in gulpfile.js:

const gulp = require('gulp');
const myPlugin = require('gulp-myplugin');

gulp.task('transform', () => {
    return gulp.src('src/*.txt')
        .pipe(myPlugin())
        .pipe(gulp.dest('dist'));
});

Changes to the plugin’s source show up immediately in the consuming project — no reinstall, no republish.


The gotcha to know about

If the linked package and the consuming project both depend on the same library (React is the classic example), npm link can resolve them to two separate copies in node_modules instead of one shared instance. That shows up as confusing runtime errors — “Invalid hook call” being the most common React-specific version — that look like a real bug but are actually a symlink resolution quirk. If you hit that, it’s usually fixable by adding the shared dependency’s path to your bundler’s resolve.alias config, or just switching to the file: protocol for that particular case.


Confirming what actually got linked

Worth checking before spending time debugging why a change to the plugin “isn’t showing up”: confirm the consuming project is actually resolving to the symlink and not a stale registry-installed copy sitting alongside it. npm ls against the specific package name shows exactly what’s resolved, symlink target included:

npm ls gulp-myplugin

# Or list every linked package in the project at once
npm ls --link

A genuinely linked package shows an -> arrow pointing at the source folder in the output; a regular registry install just shows the version number with no arrow. If the expected arrow isn’t there, the most common cause is a prior npm install in the consuming project silently overwriting the symlink with a real installed copy — worth checking for specifically after any dependency install, not just when a change first fails to show up, since it’s easy to run npm install for an unrelated reason and unknowingly break the link.


Unlinking, and how other package managers differ

Reversing a link is two commands, one in each direction — and it’s easy to forget the second one, which is how a stale symlink ends up silently shadowing what should be the registry-installed version months later:

# In the consuming project: remove the symlink, reinstall the real package
cd my-gulp-project
npm unlink gulp-myplugin
npm install gulp-myplugin

# In the package itself: remove it from the global link registry
cd gulp-myplugin
npm unlink

If the project uses Yarn or pnpm instead of npm, the commands are similar (yarn link / pnpm link --global) but not interchangeable — each tool manages its own separate global link registry, so a package linked via npm link won’t show up if the consuming project runs yarn link. pnpm is worth calling out specifically: because it uses a content-addressable store and strict, non-flat node_modules structure by default, it’s noticeably less prone to the duplicate-dependency-resolution issue described above — one of the reasons teams migrate to it for projects with a lot of interdependent local packages.


npm workspaces: try this first

Before reaching for npm link at all on a new project: if the packages in question live in the same repo (or can), npm’s built-in workspaces feature (npm 7+) has mostly replaced manual linking for this use case. Declare the sub-packages in the root package.json, and npm links them into each other automatically on npm install — no separate npm link step, no forgetting the unlink step later, no global link registry to keep track of:

{
  "name": "my-monorepo",
  "workspaces": ["packages/*"]
}

Run npm install once at the root, and any package under packages/ that depends on a sibling package resolves to the local source automatically, live-updating the same way npm link does. The honest caveat: workspaces handle the linking and dependency-sharing part, but they don’t do task orchestration — build ordering by dependency graph, caching, affected-project detection. That’s still what Nx, Turborepo, or Lerna are for. For most two-or-three-package setups, though, workspaces alone are enough, and it’s worth trying before reaching for a monorepo tool or for manual npm link.


Alternatives
  • npm pack: builds a real .tgz tarball, installed via npm install ./my-package.tgz — more stable than a symlink, but manual.
  • file: protocol in package.json: "my-package": "file:../my-package" — consistent local dependency resolution without the symlink quirks.
  • Monorepos (Nx, Turborepo, Lerna): the right call once you need task orchestration on top of what workspaces already give you — build caching, dependency-aware task ordering, affected-project detection.

Changes to the plugin’s source show up immediately in the consuming project — no reinstall, no republish.

When would you actually use this?
  • Rapidly iterating on a package used by more than one local project at once, like the Gulp plugin above.
  • Debugging a dependency issue that only shows up in a real consuming project, not in isolation.
  • Skip it for production releases (use npm publish), CI/CD (use npm pack instead — symlinks don’t survive most CI environments cleanly), or once you’re managing several interdependent packages (that’s what monorepo tooling is for).


That’s the whole picture. Questions? Comment below.


App Catalog Authentication Automation Backup Compliance Content Type CSS Flows Google Javascript Limitations List Metadata MFA Microsoft Node NodeJs O365 OneDrive Permissions PnP PnPJS Policy PowerApps Power Automate PowerAutomate PowerPlatform PowerShell React ReactJs Rest API Rest Endpoint Security Send an HTTP Request to SharePoint SharePoint SharePoint List SharePoint Modern SharePoint Online SPFX SPO Sync Tags Teams Termstore Versioning

Leave a Comment

Your email address will not be published. Required fields are marked *