symlink-monorepo

Single command to setup a monorepo using symlinks.

Usage no npm install needed!

<script type="module">
  import symlinkMonorepo from 'https://cdn.skypack.dev/symlink-monorepo';
</script>

README

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: "quot;] - the module scope, e.g. _shared maps to import('$/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.