symlink-monorepo
Single command to setup a monorepo using symlinks.
install
The usual way:
npm install symlink-monorepo
use
It's a CLI tool (see below for details) so use it like:
symlink-monorepo --root=example-monorepo --folderPrefix="_" --npmPrefix="@"
-
root: String
[optional] - the monorepo root, typically leave this blank. -
folderPrefix: String
[default: "_"] - the folder name prefix to use for symlinking -
npmPrefix: String
[default: "$"] - the module scope, e.g._shared
maps toimport('$/shared')
With that primer, let me tell you why this exists.
the problem
Many monorepo projects are set up like this:
root
├┬ shared
│└─ util.js
└┬ apps
├┬ app1
│└─ server.js
└┬ app2
└─ website.js
If you want to import shared/util.js
from inside apps/app1/server.js
you could use
relative paths:
// apps/app1/server.js
import util from '../../shared/util.js'
But that definitely gets unwieldy pretty fast.
partial solution
With the power of the NodeJS module resolution algorithm, you can embed a node_modules
folder
within your project to use absolute paths:
root
└┬ apps
└┬ node_modules
├┬ shared
│└ util.js
├┬ app1
│└─ server.js
└┬ app2
└─ website.js
Then, to import apps/node_modules/shared/util.js
from inside apps/node_modules/app1/server.js
you
can use non-relative paths:
// apps/node_modules/app1/server.js
import util from 'shared/util.js'
The good news: this is fully supported with native NodeJS, so no bundler required.
The bad news: it feels a little clumsy, and some bundlers and tooling get really hung
up on the sub-folder named node_modules
–they often filter by anything that has
node_modules
in the path.
better solution
Using symlinks, you can use symlinks to point to the appropriate folders, which looks like this:
root
├┬ shared
│└─ util.js
└┬ apps
├┬ app1
│├┬ node_modules
││└┬ prefix
││ └ shared ⇒ /shared
│└─ server.js
└┬ app2
├┬ node_modules
│└┬ prefix
│ └ shared ⇒ /shared
└─ website.js
So now we have the flat file paths again, without the embedded node_modules
, and to import
shared/util.js
from inside apps/app1/server.js
we can also use non-relative paths:
// apps/app1/server.js
import util from 'prefix/shared/util.js'
this tool
This is a CLI tool that assumes the following folder structure (more about prefixes later):
root
├┬ _shared1
│└─ # files and/or folders
├┬ _shared2
│└─ # files and/or folders
└┬ apps_folder
└┬ app1
├┬ _lib1
│└ # files and/or folders
├┬ _lib2
│└ # files and/or folders
├┬ node_modules # generated by this CLI tool
│└┬ @ # the prefix
│ ├ shared1 ⇒ /_shared1
│ ├ shared2 ⇒ /_shared2
│ ├ lib1 ⇒ /apps_folder/app1/_lib1
│ └ lib2 ⇒ /apps_folder/app1/_lib2
└─ # files and/or folders
The apps_folder/*/node_modules
folders in this case would be generated using:
symlink-monorepo --folderPrefix="_" --npmPrefix="@"
Now let's break down what this is all about:
folder prefix
Folders that are at the repo root level, and at each app level, that are prefixed
with the folderPrefix
property, are symlinked.
Most of the projects that I work with use the underscore (aka _
) character, like the
earlier example, eg. /_shared1
and /apps_folder/app1/_lib1
.
Although it's common to have a single folder like _shared
it is also common to have
multiple folders, e.g. one for services, one for controllers, etc.
npm prefix
Each of these folders is symlinked to a single "scope" name, e.g. the @angular/cli
scope name is @angular
, so if you set --npmPrefix="@"
the import name would be e.g.
@/shared
or @/lib
etc.
Specifically, the folderPrefix
gets stripped from the folder name as part of the symlink.
So if you set the folderPrefix
to _
and the npmPrefix
to $
than if you had a
file at _controller/util.js
you would import it with @/controller/util.js
. Or if you
had a file at apps/app1/_lib/util.js
you would import with @/lib/util.js
.
Note: because of this naming convention, you cannot have an app symlinked folder,
e.g. apps/app1/_shared
that has the same name (in this case _shared
) as a root folder.
To do so would not be possible, so it'll throw an error.