For people that are new to monorepos like me, there are a lot of struggles that you will face and no one talks about them in the articles you can find out there, especially when it comes to monorepos with NextJS apps.
Setup a monorepo with NPM or Yarn
You can setup monorepos in various ways: with npm
, yarn
, pnpm
, nx
, lerna
, bit.dev
, etc. but since my project doesn't need to maintain/publish packages to the npm repository, I chose to go simple and try with npm and yarn.
To add monorepo support in npm or yarn, you simply have to add a workspaces
entry to your package.json
file in the root directory of your monorepo project, like this:
{
"private": true,
"workspaces": [
"packages/*"
]
}
Initially, I wanted to have apps/*
and packages/*
separately, but that was just making my tooling & scripts more complicated, unnecessarily. So I decided to put everything (NextJS apps and shared packages) under packages/*
, for simplicity.
Now here comes the tricky part, because I had a very hard time understanding how could I reuse my packages (written in TypeScript and CSS modules) in my NextJS apps.
The npm
workspaces documentation is very deficient and doesn't tell anything about how to import your packages. So I thought I could just include them as a dependency, using local package resolution:
{
"dependencies": {
"@myorg/designsystem": "file:../designsystem"
}
}
Then you run npm install
and your local package is hard-linked in the node_modules
folder of the package that requires it.
With yarn
, instead of the file resolution, you would use workspace:*
.
The difference between yarn and npm when it comes to workspaces is that yarn is more efficient: it keeps a single node_modules
folder in the root, instead of one per package (like npm does). Yarns also prune the dependencies automatically.
The only problem with having a single node_modules is when you or some of your dependencies require a different version of React. When some of your packages need a different version of a dependency that is shared with other of your packages, you end up with the same package in the root node_modules and in your package's node_modules folder that needs a particular version.
This can cause problems, especially in NextJS apps, which don't like to have another version of React in the parent node_modules
, so you need to take special care of that.
So, after understanding how dependencies were working for each package manager, then the problem came when I tried to import the source files (in TS) of my packages. The compiler will complain that there is no loader configured for that (sorry, what??).
Yes, I had to compile my packages to JS before they could be used by the others. Somehow I felt this wasn't the right approach and that it wasn't going to work for me, as it was making my developer experience very bad and frustrating.
If you try to import TypeScript files directly, that won't work, unless you enable NextJS experimental features to include external files and configure your tsconfig.json
files to include all packages TS/TSX.
I tried that and it felt ugly and hacky. CSS Modules don't work with this approach.
The official Vercel approach for monorepos
I was about to surrender and abandon the idea of having a monorepo with my related NextJS apps,
and then I found this official Vercel example of how to configure workspaces with yarn
.
It saved my life. You only need to configure your workspaces with yarn, and use the next-transpile-modules
NextJS plugin to start being able to import your packages directly.
No manual compilation steps, no need to add it as a dependency, and no need for tsconfig.json changes! All works out of the box: JS, JSX, TS, TSX, including JSON and CSS Modules.
This is the configuration I used for my NextJS apps:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true
}
const sharedPackages = ["../designsystem", "../core"]
const withTM = require("next-transpile-modules")(sharedPackages) // monorepo support
// withTM needs to be the further-most plugin you wrap your config with.
module.exports = withTM(nextConfig)
After this, you can import your packages using their package name:
import { Button } from "@myorg/designsystem/forms"
You only need to have a index.ts
or index.js
file in the root of every package (even if they just have an empty export), in order for the next-transpile-modules
to work, but you can organize your code in folders, you don't need to export everything in the main index file.
I still need to figure out whether this will also work inside NextJS server-side scripts (e.g. importing node js/ts files of my packages in pages/api/*
files), but for now this solution covers all the needs I had of reusing my components in different NextJS apps.
It works so seamlessly that every time you change your packages, NextJS will detect that and make a hot reload while you are developing, which makes the developer experience really great.
Deploying the apps of your monorepo on Vercel
Once you are done, to deploy every NextJS separately in Vercel, you only need to configure the root directory in your Vercel app's settings. For example, if your app is under packages/myapp
, this should be the value in the Vercel settings.
You also should set up the Git Ignore Build Step section, to ignore the build if the files of your app directory didn't change, by adding git diff HEAD^ HEAD --quiet .
.
Otherwise, new deployments will be triggered even if you changed files of other apps. If you changed a component dependency that is under another package folder, then you will need to trigger the build manually or write a more advanced git check.
To know more, check this guide on Vercel's website and this other article from them.