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
.yalcdirectory 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
.tsfiles - invokes
tsc(TypeScript Compiler) - we've configured to do
yalc pushafter 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-libfrom local Yalc repository into the.yalcdirectory of your Git repo - creates / updates the
yalc.lockfile in your Git repo to keep track of original installed version ofmy-component-lib - registers this installation inside the local repository
- finally changes
package.jsonso that version ofmy-component-libis 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_modulesto the.yalcdirectory of your Git repo. Without theyalc link.. the consequentnpm installcommand fails due toMissing write accessto./.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_modulesfolder, and therefore requires annpm installto 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-libentry inpackage.json;this differs fromnpm unlink.. that would actually remove the wholemy-component-libentry from thepackage.jsonfile - removes
my-component-libdirectory from.yalcdirectory; will also completely removes the.yalcdirectory ifmy-component-libwas only (remaining) package installed - removed
my-component-libentry fromyalc.lockfile; will also completely removes theyalc.lockfile ifmy-component-libwas only (remaining) package installed
npm install: causes the original version ofmy-component-libto 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.
Frameworks at Crossroads Europalaan 93, 3526 KP Utrecht