Yalc, the `npm link` alternative that "does" work

by Emil — 8 minutes

Early on, when developing our web app, we also created a library for our common UI components, to later be able to reuse those for other web applications.

Even though we were using a separate library, we still wanted to incorporate any local changes made in our library, without first having to release a new version of it.

At first when we wanted to locally integrate our library into our web app, we tried the go-to solution of npm link. The advantage of using npm link is that it uses a symbolic link to seamlessly integrate the Git repo of our library inside the node_modules folder of our web app. And since it uses a symbolic link there is no need to copy files into the node_modules folder.

Installing .tgz file created with npm pack

As it turns out, using npm link for a library with React components is not supported, at least not out of the box, and gives you the following error after starting your local dev server:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
This could happen for one of the following reasons:
- You might have mismatching versions of React and the renderer (such as React DOM)
- You might be breaking the Rules of Hooks
- You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

Although fixing the error isn’t that complex for a custom webpack build , using Create React App (CRA) you will have to resort to ‘hacky solutions’ using either customize-cra or craco.

Therefore we decided to create a local .tgz archive file of our library using npm pack. Which we then had to be manually npm install-ed every time it was updated. And finally we also had to restart the local dev server of CRA, in order to pick up the newly installed NPM package.

Introducing Yalc

The Yalc tool offers a nice middle ground between npm link and installing npm pack-ed .tgz files.

Just like npm link it uses symbolic links but instead Yalc keeps “installations” copies inside a .yalc directory located in the root of your Git repo (like our web app) and then Yalc makes links to an installation inside that .yalc directory instead.

Besides that .yalc directory in the root of your Git repo, Yalc also keeps a local repository inside your user home directory (~/.yalc on macOs / Linux or %USERPROFILE%\AppData\Local\Yalc on Windows). Inside this local repository, Yalc also keeps track of all its “installations”, allowing it to automatically “push” updates to all copies inside the .yalc directories located in Git repos.

Yalc installation

Since the Yalc tool and its local repository are always used by multiple Git repos, with each its own node_modules, it’s best to just globally install Yalc.

Yalc can be installed (and used) using NPM::

npm install -g yalc

Or alternative using Yarn:

yarn global add yalc

Publishing to local Yalc repository

To publish & push local changes of our library we only have to execute the following on the command-line:

yalc push

Which is a shorthand for the following:

yalc publish --push

By executing yalc push or yalc publish --push, Yalc will:

  • first deploy our library to its local Yalc repository
  • and then update all of the installations of our library; which updates the copy inside the .yalc directory of our web app.

Re-publish upon changes

Whenever we make a local change to our library, we also want its TypeScript sources to rebuild, followed by an automatic yalc push.

In our case we are using the TypeScript Compiler (tsc) to compile / transpile our sources. To automatically rebuild upon a local change, we use a third-party tool tsc-watch that:

  • watches for changes in .ts files
  • invokes tsc (TypeScript Compiler)
  • we've configured to do yalc push after a successful build

To easily build our library we created the following NPM scripts:

"build-library": "tsc",
"build-library:watch": "tsc-watch --onSuccess \"yalc push\"",

Installing from local Yalc repository

Although the usage of Yalc isn’t that difficult, it turned out to work differently than expected when installing a package from its local repository.

As opposed to using npm link my-component-lib for linking / installing our library called my-component-lib, using Yalc requires the use of three consequent commands:

  1. yalc add my-component-lib:

    1. copies my-component-lib from local Yalc repository into the .yalc directory of your Git repo
    2. creates / updates the yalc.lock file in your Git repo to keep track of original installed version of my-component-lib
    3. registers this installation inside the local repository
    4. finally changes package.json so that version of my-component-lib is changed to a file: URL:
      "my-component-lib": "file:.yalc/my-component-lib"
  2. yalc link my-component-lib: does nothing more than creating a symlink from the node_modules to the .yalc directory of your Git repo. Without the yalc link.. the consequent npm install command fails due to Missing write access to ./.yalc/my-component-lib/node_modules

  3. npm install: ensures that all transitive dependencies are actually installed. Unlike when using npm link, a Yalc installed package does come with a nested node_modules folder, and therefore requires an npm install to install transitive dependencies.

To simplify the installation of a package using Yalc, I’ve created an NPM script to link my-component-lib:

"link:my-component-lib": "yalc add my-component-lib && yalc link my-component-lib && npm install",

This way you can run the three consequent commands at once using:

npm run link:my-component-lib

Consequence of running the NPM script link:my-component-lib (from above) is that it causes the value of ”my-component-lib” entry of the package.json file to be changed to "file:.yalc/my-component-lib". This behaviour, which is actually caused by ”yalc add my-component-lib” part of the NPM script, differs from npm link which does not make any changes to the package.json.

Personally I really like this behaviour of Yalc, since it makes it much easier to see if you have any Yalc installed packages, simply by diffing the local changes of your package.json.

Auto rebuild on local dev server

As it turns out, the local dev server of CRA (Create React App) does automatically picks up any changes inside a node_modules sub-directory that is symlinked. So changes inside the .yalc/my-component-lib directory will lead to automatic rebuild.

However since the package.json is only scanned at start-up of the local dev server, it still has to be restarted once the node_modules sub-directory is symlinked.

Updating the .gitignore file

Both the .yalc directory and yalc.lock file that Yalc creates in the root of your Git repo are not intended to be committed to Git.

Therefore it’s probably best to add them to the .gitignore file:

# Yalc

Making the above changes to the .gitignore file has the added benefit that an accidentally committed file: URL in your package.json causes the npm install / npm ci on your build pipeline to fail, since the .yalc directory will never be committed to Git.

Removing Yalc packages (elegant way)

The elegant approach to remove our Yalc installed my-component-lib package is to use the following consequent commands:

  1. yalc remove my-component-lib:

    1. restores the original value of my-component-lib entry in package.json;this differs from npm unlink.. that would actually remove the whole my-component-lib entry from the package.json file
    2. removes my-component-lib directory from .yalc directory; will also completely removes the .yalc directory if my-component-lib was only (remaining) package installed
    3. removed my-component-lib entry from yalc.lock file; will also completely removes the yalc.lock file if my-component-lib was only (remaining) package installed
  2. npm install: causes the original version of my-component-lib to be re-installed

To simplify the removal of a Yalc installed package, I’ve created an NPM script to unlink my-component-lib:

"unlink:my-component-lib": "yalc remove my-component-lib && npm install",

This way you can run the two consequent commands at once using:

npm run unlink:my-component-lib

Removing Yalc packages (dirty way)

Since often you will be replacing Yalc installed packages with a just released version, I often tend to just manually change the version in the package.json followed by a npm install.

This of course doesn’t clean-up the .yalc directory and yalc.lock, but since those are .gitignore-d anyway, I don’t really care that they keep lingering around.

Yalc for the win

At my current project we are currently using Yalc to locally link multiple libraries, without any issues.

In my opinion, Yalc is a viable alternative to npm link that, from my experience, always does work. And opposed to creating .tgz files with npm pack and having to do a npm install.. upon every change, using Yalc is so much more convenient due to its "push" functionality.