Yalc, the `npm link` alternative that "does" work
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:
yalc add my-component-lib
:
- copies
my-component-lib
from local Yalc repository into the.yalc
directory of your Git repo - creates / updates the
yalc.lock
file in your Git repo to keep track of original installed version ofmy-component-lib
- registers this installation inside the local repository
- finally changes
package.json
so that version ofmy-component-lib
is changed to afile:
URL:"my-component-lib": "file:.yalc/my-component-lib"
yalc link my-component-lib
: does nothing more than creating a symlink from thenode_modules
to the.yalc
directory of your Git repo. Without theyalc link
.. the consequentnpm install
command fails due toMissing write access
to./.yalc/my-component-lib/node_modules
npm install
: ensures that all transitive dependencies are actually installed. Unlike when usingnpm link
, a Yalc installed package does come with a nestednode_modules
folder, and therefore requires annpm 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
/.yalc
yalc.lock
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:
yalc remove my-component-lib
:
- restores the original value of
my-component-lib
entry inpackage.json
;this differs fromnpm unlink
.. that would actually remove the wholemy-component-lib
entry from thepackage.json
file - removes
my-component-lib
directory from.yalc
directory; will also completely removes the.yalc
directory ifmy-component-lib
was only (remaining) package installed - removed
my-component-lib
entry fromyalc.lock
file; will also completely removes theyalc.lock
file ifmy-component-lib
was only (remaining) package installed
npm install
: causes the original version ofmy-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.