Firebase Functions with Yarn workspaces - node.js

We're starting to adopt a monorepo setup using yarn workspaces and we'd like to have our firebase functions inside it. The repo structure is something like:
repo
node_modules <- all dependencies
packages
core
commom
functions <- firebase functions
So, I have 2 problems with this setup:
The dependencies of the functions don't live on the same folder as the entry file from functions
The functions depends on other packages such as core and commom that are in the repo so yarn symlinks from node_modules to the packages in the repo.
Is there anyway I can handle this?

With Yarn 2 node_modules aren't fetched and placed into in the respective functions directory (as it would be the case with calling npm i in the functions directory). So when calling firebase deploy --project default --only function the node_modules folder is missing and firebase will complain about this and abort the deployment process with the following error (or similar):
Error parsing triggers: Cannot find module [...]
Try running "npm install" in your functions directory before deploying.
There are two github issues that are tracking this issue at the moment:
Support mono-repos in deployment
Functions deployment fails when firebase-functions has been hoisted by a monorepo manager like yarn/workspaces or lerna
In the two issues above, several clever workarounds are presented by firebase users, e.g. using webpack to create a build that contains all the local packages in the release or using rsync or other tools that rewire the packages before release.
Another solution is not hoisting your project packages, if that is possible. You can do this, be adding the following two directives to your .yarnrc.yml file.
# yarnrc.yml
# disables yarn's plugnplay style and uses node_modules instead
nodeLinker: node-modules
# makes sure the node_modules are not hoisted to the (monorepo) project root
nmHoistingLimits: "dependencies"
The two directives above are explained in the yarnrc configuration docs as follows:
nmHoistingLimits Defines the highest point where packages can be hoisted. One of workspaces (don't hoist packages past the workspace that depends on them), dependencies (packages aren't hoisted past the direct dependencies for each workspace), or none (the default, packages are hoisted as much as possible). This setting can be overriden per-workspace through the installConfig.hoistingLimits field.
nodeLinker Defines what linker should be used for installing Node packages (useful to enable the node-modules plugin), one of: pnp, node-modules.

The solution I found for this is Yarn's nohoist option in your root package.json file.
By default Yarn hoists dependencies to the root directory so they can be shared between your packages. Unfortunately this will not work with Firebase. This means you need to tell Yarn not to hoist the dependencies used by your Firebase functions.
The documentation for nohoist is less than ideal, but here is an official blog post about it here:
https://yarnpkg.com/blog/2018/02/15/nohoist/
You probably want something like this:
{
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"functions/core",
"functions/common",
"functions/**"
]
}
}
Keep in mind that this uses the name field used in the package.json files of each workspace package. So in this example, it is assume that the functions directory has a package.json with "functions" as it's name.
functions/** tells yarn not to hoist any of the dependencies specified in packages/functions/package.json. This doesn't work for your shared yarn packages though, so functions/core and functions/common need to be specified separately.
You also need to include your workspaces as dependencies in your functions project, so add them to your package.json:
{
"name": "functions",
"dependencies": {
"core": "*",
"common": "*",
}
}
Once you have added all that, you should delete your packages/functions/node_modules directory and run yarn install. After doing this, you should see all your dependencies included in packages/functions/node_modules (not symlinks).

I am not sure I understand the question exactly, but I could give you my two cents on yarn workspaces based on whatever I understood from your question and from my experience using it.
Yarn workspaces consolidate all your dependencies into the node_modules present in project root as well as in a single package-lock.json to reduce conflicts and enables yarn to optimize the installation process giving you a faster yarn install. And also another advantage of it is, with a single pass yarn install can install dependencies of all packages under the workspace.
Edit: I think for some reason yarn link is not being called and instead only yarn install is being run, which will search the npm registries and throws the error mentioned in comment since it can't find the mentioned package on npm registry. So for a solution try creating an entry in the firebase's package.json like
"dependencies": {
"a": "file:../dependency-package-name/",
}

Related

Project with npm workspaces doesn’t work as dependency

I have a library that uses workspaces to house a bunch of plugin modules inside the library repo.
The dir structures looks something like:
package.json
src/app
src/plugins/package-a
src/plugins/package-a/package.json
The workspace modules are specified in the library’s package.json using local paths:
"workspaces": [
"src/plugins/package-a",
…
],
"dependencies": {
"package-a": "src/plugins/package-a"
…
}
It appears to work fine, when I npm install everything installs.
However since this is a library, the way it gets used is by being a dependency of other projects.
When I setup a separate project and add the library as a dependency, and npm install the separate project it throws an error. The error says the plugin doesn’t have a package.json, but it clearly does have a package.json.
Why can’t the project be used inside another project as a dependency?

How can I have multiple targets/executables in a single Node.js repo?

I have a React Native app built using TypeScript, and I would like to also develop a number of CLI tools to help developers and 'back office' folks, also using TypeScript. I would like these to live in the same monorepo.
Based on advice from colleagues and my own research, I have tried doing this by creating a subfolder in the repo, and creating a second package.json (and all the other config files), and npm install-ing as if it were a completely separate project. It didn't take long for this to become a total mess, primarily with duplicate imports where one thing mysteriously seems to import modules from the other targets' node_modules, but also just remembering to re-npm install all the different subprojects before each commit, etc. It gets even more confusing with the TS build folders lying around; they're another place for people to import the wrong thing from. The confusion caused by this approach has been a significant drain on productivity, and it feels like there has to be a better way. So that's the question:
What is the best practice for building multiple TS/Node targets (and by "targets", I don't mean ES6 vs ESNext, I mean in the C/C++ sense: multiple output "programs", so in this case I want it to both create the bundle necessary for my RN app, but then also generate a CLI executable.) that all share code with one another, from a single monorepo?
If it matters, I am also using Expo.
You're essentially describing a monorepo. pnpm has fantastic tooling out of the box for this.
Download the pnpm CLI and install it:
$ npm i -g pnpm # Download pnpm
$ mkdir monorepo # Create a new monorepo folder
$ cd monorepo
$ mkdir packages # This will be the folder where you add your
# apps, libraries, etc.
$ touch package.json # Create a package.json
$ echo "{}" > package.json
Create a pnpm-workspace.yaml file and add the following:
packages:
- 'packages/**'
Congratulations. You now have a monorepo where you can add multiple apps. Here's how it works:
Every folder under packages that has a package.json file is now a Node app that you can control using pnpm from the root of your workspace.
When you run pnpm i from the root, it will install all of the dependencies for all of your apps and libraries.
You can even install libraries that you create locally without needing to run npm link, or deal with adding file:../ to your package.json or any of that mess.
pnpm also supports running scripts across all of your repos at the same time.
I made an example project for you to look at. In this example, you'll notice I created a library named my-library and a dependent app named awesome-app. When I ran
$ pwd
~/stackoverflow/packages/awesome-app
$ pnpm i my-library
pnpm knew to pick up my workspace library automatically:
{
"name": "awesome-app",
"dependencies": {
"my-library": "workspace:0.1.0"
}
}
Note the workspace:0.1.0 that matches the version of the package I have in my repo.
I love pnpm. It isn't without its faults, but I've been very productive with it. There are also other alternatives, such as Lerna, and npm and yarn support their own workspace protocol. I just find pnpm to work the best and has the least amount of ceremony.
Enjoy.

Is there a way to specify different paths for the same dependencies in package.json?

I am working on an npm package that includes an example directory to run/test the actual package. In the example directory, I have included back the parent package using "file:..".
This works fine when developing and making frequent changes to the parent package, but if I want to use the example as a stand-alone app, I would need to point to the actual npm package.
Is there a way to have "2 configs" in the same package.json:
one that points to `"file:.." for local development
one that points to the npm package to use as a stand-alone app
This would avoid duplicating the example directory
You could do this with lerna which is a mono-repository CLI tool.
First of all, you would have to define multiple projects within the same repository. Lerna calls these projects "packages" and stores all of them within a /packages folder.
package.json
/packages
/my1stPackage
package.json
/my2ndPackage
package.json
Lerna has all kind of optimizations, which I won't dive in too deep here. But there are a couple of basics:
to initially install all dependencies of all repos, run lerna bootstrap --hoist.
You can still run npm run ... as before, but those refer to your root package.json file. To run npm scripts for specific sub-package you should run lerna run <args> -scope=<packageName>. (e.g. lerna run build --scope=my1stPackage)
You can add shortcuts for this in the root /package.json script section.
"scripts": {
"setup": "lerna bootstrap --hoist",
"build:my1stPackage": "lerna run build --scope=my1stPackage"
}
What will interest you most, is that sibling packages can just reference each other from their specific package.json to include each other as dependencies.
So, let's assume that my1stPackage uses my2ndPackage. Inside the package.json file of my1stPackage there would be something like
"dependencies": {
...
"my2ndPackage": "^0.0.1"
}
The my2ndPackage could actually be a package which is published in npm. However (!) while developing locally, lerna will add a symbolic link inside the /packages/my1stPackage/node_modules/my2ndPackage, which points to the folder of /packages/my2ndPackage. (And that really does work on all relevant operating systems.)
You package.json looks the same for local development as it does for people who download your package through npm. However, it's your lerna setup that fixes this with this symbol link.
I found two potential ways to do this:
npm link : https://docs.npmjs.com/cli/v7/commands/npm-link/
npm workspaces : https://docs.npmjs.com/cli/v7/using-npm/workspaces
But in my specific case, there are packages that can conflict between the parent and child (example) packages.
I couldn't find a robust way to make it work and decided that the simpler approach would be to simply create a separate repository that would contain a stand-alone version of the example directory and a script that can keep it up to date with the "master example" in the original repository. This way development stays fast and the "example copy" is easy to keep up to date without duplicating code.

Pointing the main field in package.json conditionally

I have a Monorepo under Lerna and Yarn Workspaces. The repo has packages which are published to npm and consumed outside the monorepo as well as within the monorepo. While developing in the monorepo we would like the main field of package.json for all such packages to point to the src directory, while when a package is used outside the monorepo, we would like the consumer to use the transpiled code in the dist folder.
I want this to be consistent across all uses of the packages, My current solution is to have the main field point to the dist folder. Then for each of the tools within the monorepo, namely jest, tsc, webpack, parcel I've had to come up with a different tool specific solution for aliasing the src directory instead of the dist directory. But I don't like the fact that I've had to do this work for each of these tools. It just doesn't seem scalable.
Has anybody come up with a lower level solution, where a module resolves to a different folder based on the environment?
Thank you.
If your internal code base is always transpiling the sources, why not just import { thing } from "my-package/src/main.js?
Then you can just leave the main field as dist for the consumers who ideally shouldn't have to keep track of additional paths when importing your packages.
There are a lot of details left our in your answer, but assuming you're using a single webpack/other instance to compile all your packages.
Another approach, since you're already coupled all your packages via the same compilation step, why not just use relative paths between the packages? That way you'll never have to act as a consumer but with slightly different needs.
And finally the third approach, which I think sounds a bit convoluted but should do exactly what you're asking for. Create a script that uses globby or some other npm package to grab all package.json files in your repository (excluding node_modules!). require() / iterate through these package.json manifest files and set the main field to an input value (say "dist"). Then, create two bin js files (hint: bin field) called set-main-dist and set-main-src, and possibly a third called unset-main.
Next, no matter what scripts you run in your package.json files at the root (or using lerna run), make sure to let the script look either like this:
"prebuild": "set-main-src"
or like this
"build": "set-main-src && build etc"
Hope one of these options work out for you. Remember that it's rarely worth going against the stream of usual patterns in tooling and what not. Make this one worth it.
I had exactly the same dilemma but with yarn3.
The solution importing always from source dint worked for my case, as the package itself might get published to npm too.
So after digging around I luckily found the package.json property publishConfig.main https://yarnpkg.com/configuration/manifest#publishConfig
With it I can change the main field from source to dist on npm publish.
Basically only for publishing we use a modified package.json.
Implemented in my package.json it looks like this:
{
"main": "./src/index.ts",
"publishConfig": {
"main": "./dist/index.js"
}
}
and when I run yarn npm publish or yarn pack the main field will be replaced temporary for the zip.
While all my tooling (jest and ts) can still rely on the main field pointing to the source.

How are npm packages managed in nodester?

I don't understand how node packages are managed in nodester. When I run nodester npm install <package-name> from CLI I don't see any packages in my app's source folder. Without these packages visible in my folder, can I use them in usual way (as if I had I installed them in my apps folder directly).
I am advised against storing packages directly in the folder since Nodester offers Node PaaS for free and it would be unkind to not optimize my app and make it use minimal space.
Secondly is there a way through which I can run the app both locally and on nodester. How can I tell git not to push the locally installed git modules. I have heard something like git ignore. How do I manage git ignore so that my local packages are not pushed on nodester?
I might not have been eloquent in framing the question as I am a newbie to node so anyone who can put my question in a better way, feel free to Edit this.
Generally the best way is to add the node_modules dir to your .gitignore file. My .gitignore looks like this for my node projects:
*.sw*
.DS_Store
node_modules
The first line ignores any Vim temp files, the second to ignore OSX .DS_Store files and the last one ignores my node_modules dir. You will need to delete your node_modules dir from your repo first using git rm if its already committed.
More explination re. gitignore files is here from GitHub.
So that will make Git disregard your node_modules, awesome. Secondly, you will need to create a package.json file. This is what tells npm (and Nodester) what your app depends on.
{
"author": "Mr Awesome", // This is your name :)
"name": "my_awesome_app", // This is your apps name
"description": "More awesome than most other apps.", // What your app does
"version" : "0.0.1", // Your apps version (increment this when you deploy)
"node": "0.6.12", // The version of node you want Nodester to run your app on
"dependencies": {
"connect" : "2.0.3", // depend on version 2.0.3 of connect
"express" : "*" // depend on the latest version of express
}
}
More information about package.json formats can be found here:
When you push to nodester should read the package.json and install your dependencies.
Hope that helps!

Resources