symlink-monorepo v1.0.1
symlink-monorepo
Single command to setup a monorepo using symlinks.
install
The usual way:
npm install symlink-monorepouse
It's a CLI tool (see below for details) so use it like:
symlink-monorepo --root=example-monorepo --folderPrefix="_" --npmPrefix="@"root: Stringoptional - the monorepo root, typically leave this blank.folderPrefix: Stringdefault: "_" - the folder name prefix to use for symlinkingnpmPrefix: Stringdefault: "$" - the module scope, e.g._sharedmaps 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.jsIf 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.jsThen, 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.jsSo 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 foldersThe 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.