19

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:

  1. The dependencies of the functions don't live on the same folder as the entry file from functions
  2. 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?

4 Answers 4

6

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:

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.

1

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).

5
  • I tried this approach but was unable to deploy the functions. The firebase process is unable to install the dependencies (since they're local) Commented Jan 5, 2020 at 17:12
  • @ThiagoNascimento hmmm. I did this a while ago, and had forgot to post this answer. I may be forgetting another step in the process. I'll take a look. Thanks for letting me know. I'll update it if I figure anything out.
    – twiz
    Commented Jan 5, 2020 at 19:15
  • I've created a repository to demonstrate the use of this technique, however I seem to having issues when deploying the functions to Firebase, even though the web (hosting) aspect works fine. Any help would be greatly appreciated - I've added the errors to the Readme github.com/cjmyles/firebase-monorepo Commented Apr 3, 2020 at 14:46
  • 1
    I also wasn't able to get this to work with cloud functions. While node_modules contains the shared core package, the deployment of cloud functions doesn't use node_modules. So, I was left with creating a packaged version of my shared dependency and then updating packages.json.
    – R. Wright
    Commented May 6, 2020 at 13:58
  • I wasn't able to successfully run yarn install, some packages like base64 and node-waf seem to fail to run even with functions/** specified in nohoist
    – Nick
    Commented Sep 14, 2021 at 14:37
1

I have developed a solution and wrote an article about this. Below is an excerpt, but here is the full article

The problem with Firebase

When deploying to Firebase it wants to upload a folder just like a traditional single package repository, containing the source files together with a manifest file declaring its external dependencies. After receiving the files in its cloud deployment pipeline, it then detects the package manager and runs an install and build.

In a monorepo, and especially a private one, your Firebase code typically depend on one or more shared packages from the same repository, for which you have no desire to publish them anywhere.

Once Firebase tries to look up those dependencies in the cloud they can not be found and deployment fails.

Hacking your way out

Using a bundler

In order to solve this you could try to use a bundler like Webpack to combine your Firebase code with the shared packages code and then remove those packages from the package.json manifest that is being sent to Firebase, so it doesn’t know these packages even existed.

Unfortunately, this strategy quickly becomes problematic…

If the shared packages themselves do not bundle all of their dependencies in their output, Firebase doesn’t know what the shared code depends on, because you are not including or installing those manifests.

You could try to bundle everything then, but if your shared package depends on things your Firebase package also depends on, you now have one part of your code running an internally bundled copy of a dependency and the other part using that same dependency from a different location installed by the package manager.

Also, some libraries really don’t like to be bundled, and in my experience that includes the Firebase and Google client libraries. You will quickly find yourself trying to externalize things via the bundler settings in order get thing to work.

And even if you managed to make all this work, you are probably creating large bundles which could then lead to problems with the cold-start times of your cloud functions.

Not exactly a reliable or scalable solution.

Packing and linking local dependencies

An arguably more elegant approach involves packing the local dependencies into a tarball (similar to how a package would be published to NPM), and copying the results to the build output before linking them in an altered manifest file.

This could work quite nicely, as it basically resembles how your Firebase code would have worked if these packages were installed from an external domain.

Whether you’re doing this manually, or write a shell script to handle things, it still feels very cumbersome and fragile to me, but I think it is a viable workaround if your local dependencies are simple.

However, this approach quickly becomes hairy once you have shared packages depending on other shared packages, because then you’ll have have multiple levels of things to pack and adapt.

My solution

I have created isolate-package. The name is generic because it doesn’t contain anything specific to Firebase, although I currently don’t know of any other use-cases for isolated output.

It takes a similar approach to what is described earlier in packing and linking dependencies, but does so in a more sophisticated way. It is designed to handle different setups and package managers and it completely hides the complexity from the user.

The isolate binary it exposes can simply be added to the Firebase predeploy hook, and that’s pretty much it!

This also allows you to deploy to Firebase from multiple different packages and keep the configuration co-located instead of littering the monorepo root directory.

It should be zero-config for the vast majority of use-cases, and is designed to be compatible with all package managers.

4
  • I can't get isolate-package to work. Is it being maintained?
    – pomo
    Commented Aug 17, 2023 at 14:16
  • It seems not working with nesting path. e.g: serviceA imports from shared, but shared has imported from sub-folder itself. Then the isolate-package can not import correctly. - packgage/serviceA - packgage/shared
    – kollein
    Commented Aug 23, 2023 at 8:25
  • @pomo yes it is maintained, see the releases Commented Aug 24, 2023 at 15:48
  • @kollein nested dependencies are supported, but there are instructions about folder structure and manifest requirements. Please check the readme and report an issue if necessary. Commented Aug 24, 2023 at 15:48
-1

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/",
}
8
  • I know how yarn workspaces work and I'm currently using in the project. The problem that I'm having is that, when importing another local package from the same project, yarn takes care of symlinking it so instead of pointing to node_modules it will actually import from the local package. And that's the issue, firebase functions doesn't seem to work with symlinked packages that aren't in node_modules Commented Jun 18, 2019 at 17:29
  • Now the question is much clearer for me. Yes yarn symlinks to package in same workspace. But what issue are you facing exactly. It should work just like an npm installed package. Commented Jun 18, 2019 at 17:30
  • when deploying to firebase functions, I get an error that some X package (which is local) wasn't found Commented Jun 18, 2019 at 21:49
  • In the project, I am guessing yarn.lock file is there because if it is not, Google cloud installs using npm which might not do the symlinking I guess. Commented Jun 19, 2019 at 1:37
  • Yes, yarn.lock is there. Does Google cloud even support yarn? Maybe that's the point Commented Jun 19, 2019 at 1:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.