Problem
I am a fan of serverless solutions including Firebase Cloud Functions, but until now it still does not natively support monorepo and pnpm. This was a very frustrating development experience. After a few hours of research, trying, failing, and repeating the cycle, at least I can figure out a hack to solve this problem. See the problem here: https://github.com/firebase/firebase-tools/issues/653
Some references that I have read:
- https://github.com/pnpm/pnpm/issues/4073
- https://github.com/willviles/firebase-pnpm-workspaces
- https://github.com/firebase/firebase-functions/issues/172
- https://github.com/pnpm/pnpm/issues/2198
- https://github.com/pnpm/pnpm/issues/4073
- https://github.com/Madvinking/pnpm-isolate-workspace
- https://github.com/pnpm/pnpm/issues/4378
- https://github.com/pnpm/pnpm/discussions/4237
Motivation
Thanks to the community, I hope this part will make more sense for the future readers and they can choose the right approach for the right situation.
The problem that I want to solve is deploying the Firebase Cloud Functions in the CI environment. Since we only set up the CI once and CI server will handle things automatically for us.
Some important parts to make things clearer to understand how things work.
The folder structure should be like
root
|- apps
|- api
|- packages
|- core
firebase.json
pnpm-workspace.yaml
The apps/api/package.json should look like this:
{
"name": "api",
"main": "dist/index.js",
"dependencies": {
"firebase-functions": "^4.1.1",
"core": "workspace:*"
}
}
Explanation:
- The
apps/apifolder contains the Cloud Functions code base - The
packages/coreis a dependency that our Cloud Functions depend on as defined in thedependenciesfield of aboveapps/api/package.json. You can read more about theworkspaceprotocol here: https://pnpm.io/workspaces#referencing-workspace-packages-through-aliases
The apps/api/package.json explanation:
- Field
nameis MUST since it defines how module resolution works. You may familiar with pnpm command for examplepnpm install -D --filter api". Theapiis the value of thename` field. - Field
maindescribe how NodeJS resolve your code. Let's imagine when reading the code base, NodeJS won't know where to get started if you don't tell it. Set thismainvaluedist/index.jsmeans "Hey NodeJS, look for the filedist/index.jsat the same level of thepackage.jsonfile and run it".
Now let's go to the tricky part!
Hacky solution
Solution: https://github.com/Madvinking/pnpm-isolate-workspace
The idea is, to build all the dependencies into one single workspace with some tweaks in the package.json file since firebase deploy command does not support the pnpm workspace:* protocol. I tested many times in both my local environment and CI server, and as long as the package.json file contains the workspace:* protocol, it will fail even if the code is already built.
Steps:
- Build Cloud Functions locally, the output will be in
apps/api/dist - Change the
firebase.jsonsourcefield to"source": "apps/api/_isolated_",and remove thepredeployhook. Thepredeploydefine what command will run BEFORE deploying the Cloud Functions (usingfirebase deploycommand). The reason why I remove this is I already build the code base in the previous step. - Run
pnpx pnpm-isolate-workspace apiat the root folder, it will create the folder name_isolated_. - Copy build folder into new created folder
cp -r apps/api/dist apps/api/_isolated_ - Go to the
apps/api/_isolated_runmv package.json package-dev.json - Go to the
apps/api/_isolated_runmv package-prod.json package.json - Go to the
apps/api/_isolated_runsed -i 's/"core\"\: \"workspace:\*\"/"core\"\: \"file\:workspaces\/packages\/core\"/g' package.json, thanks to this comment - Finally, run
firebase deploy --only functionsat the root folder
Questions?
- Why do I need to rename two
package.jsonfiles in theapps/api/_isolated_folder? The main reason is is removing thedevDependenciesto reduce manual work for the next step- Because the
package-prod.jsondoes NOT contains thedevDependenciesand we don't needdevDependenciesfor the deployment. Other than that, thedevDependenciesmay contain some other packages from my other workspaces. - I don't know yet how to let the
firebase deploycommand using thepackage-prod.jsonfile instead ofpackage.json
- Because the
- What exactly
sedcommand does? Why do I need that?- This is the most tricky part. The
sedcommand will read the file, and replace some strings with others, which is a very low level, risky, and not easy to do for everyone. That means it only makes sense when doing this in the CI server since it is isolated to your code base. You never want to see these changes in your git repository.
- This is the most tricky part. The
- Why not install
firebase-toolsas a dependency and then run something likepnpm exec firebase deployin the CI server?- It makes sense if you run the
firebase deploycommand from your local machine. In the CI server, please note that I use this.
- It makes sense if you run the
- What actually
w9jds/firebase-actiondoes and WHY do I need to use that?- The most important part is the "authentication process". To deploy Firebase Cloud Functions, "you" need to have the right permissions. For example in your local machine, you need to run the command
firebase loginbefore doing anything, then you need to grant access. The same thing will happen on the CI server, we need to grant the right permissions to the Google Service Account through theGCP_SA_KEYkey. In the CI environment, there are no browsers to let you sign in, that's the point. So instead of manually running the commandpnpm exec firebase deployin the CI server, the abovew9jds/firebase-actionwill handle things for you.
- The most important part is the "authentication process". To deploy Firebase Cloud Functions, "you" need to have the right permissions. For example in your local machine, you need to run the command
Other notes
There are some problems with this approach, please don't think it's a perfect solution, and make sure you fully understand it because it's likely you may touch it again in the future, unfortunately.
- Every time my
apps/apirequire another new dependency from other workspaces, I need to manually do the same thing with thepackages/core - The https://github.com/Madvinking/pnpm-isolate-workspace support some CLI flags that may help you reduce the manual work, better to take a look.
- To deploy the Cloud Functions, the service account needs to have some specific roles, check out the official docs: https://firebase.google.com/docs/projects/iam/permissions#functions