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 · Unlinking and other package managers · Alternatives · When would you actually use this?


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.


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.


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’re managing more than a couple of interdependent local packages — scales past what npm link is built for.

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 *