I've been walking through the fine line between over-engineering and learning new integration possibilities with my portfolio crafting.
Because, yes I'm one of those developers who like to complicate his life for the bigger purpose of learning and "probably" implementing this knowledge in future projects.
Today's articles explain how to successfully deploy a static site with Nuxt v3.0.0 between a pnpm monorepo on Netlify.
For this tutorial, I'm assuming you have a basic understanding of monorepos, npm packages, deploying apps to Netlify, and a little bit of CI/CD. If this list sounded like phrases coming from a Chewbacca to you, consider investigating a little before coming back here to properly get some value out of it.
It sounds very specific, but if you find yourself in the same use-case, this is the article for you.
You can find the code for this repo here https://github.com/alvarosabu/pnpm-monorepo-nuxt
The initial idea
Before you think I'm crazy, this is what I wanted to achieve:
Sounds simple, a packages
folder containing different libs such as a UI library with my design system done in VueJS, a Typescript lib with utilities and Portfolio website below an apps
directory made with Nuxt v3 that will consume several resources from the packages between the monorepo.
I liked the idea of being able to create the UI and utils packages on the road and test them directly on the portfolio without the need for extra playgrounds. Then being able to publish and release npm packages from them to install it in other projects easily.
If you are not planning to publish multiple packages (public or private) then a monorepo might not be the answer for you. A simple repo with a good structure would be enough.
Choosing PNPM workspaces
At the moment I started to transform this draft of an idea into an actual thing, and did some investigation on which monorepo technologies would suit me the most.
I started with Lerna in the early phases, I was using it with yarn workspaces and the whole CI/CD process was pretty smooth using Semantic Release.
On the road, I discover pnpm as a faster alternative to npm/yarn, also several of my favorite OSS projects were using it (Vite, Vueuse, etc), I decided to give it a try.
Then Lerna announced its temporally archive due to lack of maintenance this year by its lead maintainer, later to being taken over stewardship by Nrwl, check the announcement here
Before that announcement, several users of Lerna (including me) were looking for an alternative that allowed us to easily create and maintain a monorepo of these characteristics.
Since I was using pnpm, I found out that it has a built-in support for monorepos by creating a Workspace. There it was the solution that I needed.
All you need to do is add a pnpm-workspace.yaml
with the following content:
packages:
# all packages in subdirs of packages/ and app/
- 'packages/**'
- 'apps/**'
And tweak your package.json
to add this:
// package.json
{
...
"workspaces": [
"apps/**",
"packages/*"
],
...
}
We're ready to move to the next step
Nuxt
Being a VueJS and Nuxt fanboy as I'm 🤪, jokes aside, is the framework find myself enjoying the most when coding. It wasn't a difficult choice for me to do my Portfolio with a recently stable Nuxt v3.0.0-rc.
For those of you that are not familiar with The Nuxt ecosystem, is a hybrid Vue framework that allows you to easily create SSR, CSR or SSG experiences with an amazing DX (Developer Experience)
To get started just run this command inside of the apps
directory:
pnpm dlx nuxi init portfolio
When installing the dependencies, is important to use the shamefully-hoist
flag. (I don't really fully understand why, better explained here)
pnpm install --shamefully-hoist
And voilá, by running pnpm run dev
this beautiful welcome screen showed up in my browser:
Before we jump into the Netlify config there is a final thing we need to set on our brand new Nuxt. Since I want to generate the project statically I will use nuxt generate
Netlify
This is the part where things start to get interesting 🤭.
Netlify is a cloud computing platform that offers hosting and serverless backend services for web apps and static websites.
I personally use it a lot and to be completely fair, I have never used it for such complex integration. Most of the time was to deploy sites for clients, one or another reproduction, slides for a conference talk, some OSS libraries documentation under my domain, you know, easy stuff.
So following the Netlify's documentation for monorepos we can add a netlify.toml
file on the root of the repository with the [build].base
pointing to the portfolio directory apps/blog
like this:
[build.environment]
NODE_VERSION = "16"
[build]
base = "apps/blog/"
Then we need a publish
directory so Netlify knows where the deploy-ready HTML files and assets generated by the build are located.
Whenever we run nuxt generate
, it will prompt the following:
It seems we found our directory .output/public
, we can update the netlify.toml
to:
[build.environment]
NODE_VERSION = "16"
[build]
base = "apps/blog/"
publish = ".output/public"
🤔 That should work right?
The next thing to set is the build command. This was the moment I realized that Netlify does not support PNPM right away yet (at the time of writing there is a Feature Request open).
🥲 The world seemed to collapse in front of my eyes (drama), Oh no! I will need to get back to yarn
, or even worse, to bare npm
😭 (more drama).
I stopped and thought, what would my heroes do at this moment?
Yeeeess!! Let see how Vite netlify config for docs was looking like and BAAAM 💥 there was the answer to our needs:
[build.environment]
NODE_VERSION = "16"
NPM_FLAGS = "--version" # prevent Netlify npm install
[build]
base = "apps/blog/"
publish = ".output/public"
command = "npx pnpm i --store=node_modules/.pnpm-store --frozen-lockfile && npx pnpm run generate"
We added a NPM_FLAGS
property to prevent Netlify to install packages with npm
by default. Then we use npx
to install pnpm and then run any command that we want.
And don't forget the redirects:
[[redirects]]
from = "/*"
to = "index.html"
status = 200
Let's see what happens... 🤔
If we push and check the deployment on Netlify, we will see that is complaining about Failed to resolve entry for package "@pnpm-monorepo-nuxt/ui".
Let's see how to solve all the different troubles that can happen.
Trouble Shooting
Building the UI package
As the log above explains, our blog app is not able to resolve the UI package. So we will need to build it before we generate the blog.
On our apps/blog/package.json
we can add a new script called generate:ci
//package.json
"scripts": {
"generate:ci": "pnpm run -w build:ui && nuxt generate",
}
Notice the use of pnpm run
with a -w
flag, this allows to run the command from the root.
Now let's add that command to our root package.json like this:
"scripts": {
"build:ui": "pnpm --dir packages/ui build",
}
Last step will be update the command on the netlify.toml
to this new generate:ci
[build]
base = "apps/blog/"
publish = ".output/public"
command = "npx pnpm i --store=node_modules/.pnpm-store --frozen-lockfile && npx pnpm run generate:ci"
Annnnnnnnnnnnnnnd.
#"@%·"
ok Alvaro, don't fall into eternal down-spiral of darkness... just breath.
Getting the Publish directory right
Gotta admit this part was a little frustrating for me. Following what Nuxt prompt when using nuxt generate
the static files to deploy would be inside .output/public
right?.
At this point, I was not able to find any real-life example of a Nuxt3 SSG deployed between a pnpm monorepo. All the references on internet were pretty simple, a Nuxt3 app on a simple repo, just push a Nitro preset will do the magic for you, all worked fine.
Why it was failing in this specific case? I search Netlify Monorepo Docs, Nuxt3 docs about deployment and I couldn't find any clue.
I tried every combination of things you can imagine, went to the multiverse and back, changing the base directory, the commands, everything.
So I did the unthinkable. I gave up by tweeting my frustration about the integration:
Then a Hero landed heroically into the thread:
Daniel Roe is well-known architect that works on the Nuxt Framework and an OSS saviour.
So we jumped on Discord, I created the reproduction repo and send him the logs and explained the context. (Here I want to make a special mention of the fact that Dani was on holiday, and his dedication to helping is so big, that he took the time to help 1 user, me, to solve my issue, I was amazed 😍)
After a few messages:
Issue is your dist symlink. Just remove it.
It was set to an absolute path rather than a relative one
🤔 wait what.... so maybe if I change the publish directory for ./dist
it would work? Worth trying.
😱 IT WORKED!!!! IT FINALLY WORKED!!!!
But why? Why does the publish
needs to be ./dist
and not .output/public
The answer is right here in the code of Nitro is the server-engine of Nuxt.
It's because the Netlify Preset sets it to dist
by default rather than .output
(Because that's the netlify default location).
That's intended to support zero-config. So I asked if it makes sense to add zero-config preset for deploying between a monorepo. There is already an issue opened that could make it possible.
`workspaceDir` #11074
For the conditions that nuxt app is not in the root of workspace/repository directory, often issues happen. Currently, we have an internal modulesDir
option that helps to resolve this by creating a list of directories looking for node_modules
(and by default process.cwd() is added to this list) but it is neither explicit or solving all cases.
Related issues:
- nuxt/framework#132
- nuxt/bridge#123
- https://github.com/nuxt/image/issues/235
- https://github.com/nuxt/vite/pull/138
Vite tries to find root by looking up for pnpm-workspace.yaml
or a package.json
with workspaces
key but it is not enough since there are conditions user is not using a monorepo workspace but simply have nuxt app in a sub-directory. (https://vitejs.dev/config/#server-fsserve-root)
We can implement this by looking up (from rootDir
) for first package.lock
, package-lock.json
or package.json
that has workspaces
key (unless there is a package.json
in rootDir
?) and falling back to rootDir
if couldn't find. and also giving users ability to customize it with a nuxt option.
Final Form
Until a zero-config preset for monorepos is out there, you can use this final form of the netlify.toml
to make it work 😜.
[build.environment]
NODE_VERSION = "16"
NPM_FLAGS = "--version" # prevent Netlify npm install
[build]
base = "apps/blog/"
publish = "./dist"
command = "npx pnpm i --store=node_modules/.pnpm-store --frozen-lockfile && npx pnpm run generate:ci"
[[redirects]]
from = "/*"
to = "index.html"
status = 200
Wrap up
This long experience helped me to understand a lot of things, like how pnpm monorepos work, how Nuxt is deployed on Netlify and most important:
The immense human quality and dedication of the contributors of OSS (In this case Daniel Roe from Nuxt and Zoltan Kochan of pnpmjs)
Please consider Sponsor their work in the links above.
You can find the code for this repo here https://github.com/alvarosabu/pnpm-monorepo-nuxt
It's all for today folks. Happy coding!!!
Top comments (2)
Haha, you reached the 2 most dedicated people when asking for some help. 👌🏻
Of course your issue wouldn't last more than 1hour, calling the real heroes! 💪🏻
Great that the setup now works! 🌟
Great one. will definitely give it a try