I know there are plenty of solutions for managing monorepos, Lerna, and so on. To be honest, I am not checking those anymore.
Our setup (e.g., at https://screen.studio) is, I believe, very simple and effective. I set it up, I think, 4 years ago and have never looked back.
Typescript, bundling, libraries, and apps
At the core of our setup, we have the concept of libraries and apps.
App is something the end user interacts with. In our case, it is our website and our Electron app.
Apps are responsible for bundling libraries. Libraries are simply buckets of code. Libs have no build pipelines; they don’t even have a
src folder. They are flat, with very simple package.json and TypeScript files. No configs, no build scripts, and no type definitions (more on that later). Apps will be responsible for building libs.Core packages config
At the core, we have
package.json like this:And then each package have
package.json like thisBelieve it or not - this is the entire
package.json of quite a huge part of our codebase.You could technically say it has no dependencies defined, which is not a good thing. I kind of agree, but I think out of laziness, I usually add dependencies to the root package.
Now, you run
yarn install, and magic starts to happen.@s/ui is moved to node_modules/@s/ui, but it is not copied there - it is symlinked, so technically <repo>/ui folder is mirrored with <repo>/node_modules/@s/ui - every change that happens in the source file is also present inside node_modules. This is important, as it allows for a great hot reloading experience, etc.But note, that inside
node_modules we have .ts files, which are not bundled at all - those are source files.Now, you simply import those like:
It just works. It is fully typed and hot reloaded. You don’t need
tsconfig imports magic to make root import work. It works because our package is mirrored into node_modules/@s/ui. It does not require .d.ts files at all, because there is no src or dist folder, so it simply reads types directly from the source code.The only thing you have to do is to define a proper bundling config, as often by default bundlers expect things in
node_modules to already be bundled.For example, our app called
web is using next.js and our next.config.ts needs something like this:And that’s it. Next.js now knows that it needs to transpile those TypeScript modules when they are imported. Hot reloading works out of the box. Any change inside our
ui package is instantly reflected when browsing localhost, there is no need to rebuild, update type definitions etc.Some other tricks
At the root
package.json, we have scripts like those:This allows you to manage the entire monorepo from the root folder. For example, our
web package has its own development script that will start the Next.js development server.So from the root you can do
yarn web dev and it will just work. Or if you need to add some dependency only to the web package, you do yarn web add some-packageYarn 4.5 and corepack
We try to use the latest
yarn version. Our goal, however, is to make the onboarding developer experience as smooth as possible.Not sure if you’re familiar with it, but
corepack enable is a really powerful Node command. With this one command (which is available already if you have Node installed), it will replace your global Yarn with the local one defined in the package. Thus, you can do yarn set version 4.5.0 --yarn-path, and it will create a plug-and-play copy of Yarn 4.5 inside your repo, creating .yarnrc.yml like this:And you’re good to go. In your README, your Getting Started section can look like:
- The only requirement - have
nvminstalled on your machine
- Clone repo
- run
nvm install(assume you have.nvmrcwhich you should)
- run
corepack enableto make sure you will useyarn”binary” which is inside the repo
- run
yarn install
- optional - get your
.envfile from your colleagues
- run
yarn app devoryarn web dev
- enjoy coding
