A plugin for Nx that provides support for Firebase projects in an Nx monorepo workspace.
This project was generated using the Nx plugin workspace generator v12.3.4 but should be compatible with Nx versions > 12.1.1.
See the CHANGELOG for release notes.
Note: This project is an early beta and feedback is very welcome. Please note I created this plugin to primarily serve my own needs, so feature requests may take a bit of time to consider.
I'm making this project in my spare time, so if you find it useful, or it saved you some time, you are very welcome to buy me a ☕ coffee.
Nx provides a great way to manage monorepo workflows, however if you have a development setup where your Nx workspace uses one or more Firebase projects that use different combinations and configurations of Firebase features such as hosting, storage, database rules/indexes, and functions, then some extra tooling is necessary in order to maintain a familiar Firebase workflow within your monorepo.
This plugin aims to help with this by:
- Enabling & promoting use of shared Typescript code libraries within Firebase functions
- Retaining a familiar usage of all Firebase features in a way that feels integrated with Nx workflow
- Requiring minimal friction/setup for configuration of an Nx workspace in order to be productive with development, building & deployment of Firebase projects
npm install @simondotm/nx-firebase
Installs this plugin into your Nx workspace.
nx g @simondotm/nx-firebase:init
Installs firebase dependencies (both for backend and frontend) to your root workspace package.json
(or you can just npm install
firebase dependencies manually)
nx g @simondotm/nx-firebase:app <appname> [--directory dir]
Generates a new Nx Firebase application in the workspace - /apps/[dir]/appname
The app generator will also create a Firebase configuration file called firebase.appname.json
in the root of your workspace (along with a default .firebaserc
and firebase.json
if they don't already exist)
nx build appname [--with-deps] [--watch]
Compiles & builds the target Nx Firebase (functions) application to dist/apps/[dir]/appname
. It will also auto-generate a package.json
that is compatible with the Firebase CLI for functions deployment.
(nx affected:build [--with-deps]
should also work fine).
Notes:
Using
--watch
requires at least Nx version 12.3.4 due to this issueUsing
--watch
will not (afaik) detect changes made to dependent librariesIf your functions reference any local libraries, always use
--with-deps
For inital deployment:
firebase login
if not already authenticated
firebase use --add
to add your Firebase Project(s) to the .firebaserc
file in your workspace. This step must be completed before you can deploy anything to Firebase.
Then use either Firebase CLI:
firebase deploy --only functions --config firebase.appname.json
Or
nx deploy appname --only functions
For deploying websites to Firebase Hosting see the section below
nx serve appname
- will build the functions application in --watch
mode and start the Firebase emulators in parallel
nx getconfig appname
will fetch the remote server functions configuration variables and save them locally to the app directory as .runtimeconfig.json
for the emulators to use.
An Nx Firebase application is primarily a container for firebase functions, but it also contains the various configurations for other Firebase features you might use such as storage, firestore, and real time database.
You don't have to use all of these features, but the Nx-Firebase plugin ensures they are all there if/when you do.
When a new Nx Firebase application project is added to the workspace it will generate:
Within the application folder:
- A buildable Typescript node library for Firebase functions
- Default
package.json
for the Firebase functions - Default
firestore.indexes
for Firestore database indexes - Default
firestore.rules
for Firestore database rules - Default
database.rules.json
for Firebase realtime database - Default
storage.rules
for Firebase storage rules - Default
public/index.html
for Firebase hosting - you can delete this if your firebase configuration for hosting points elsewhere.
And in the workspace root:
- A
firebase.<appname>.json
configuration file for the Firebase project, which is preset with references to the various configuration files in the application folder
It will also generate:
- A default/empty
.firebaserc
in the root of the workspace (if it doesn't already exist) - A default/empty/stub
firebase.json
in the root of the workspace (again, if it doesn't already exist)
Note: These two files must exist in the root of a workspace otherwise the Firebase CLI will complain that it needs to initialise itself.
Important: The default
firebase.json
is intentionally empty, because the idea is to usefirebase.appname.json
instead. (Although I'm reviewing this atm as it seems slightly unintuitive for single firebase project workspaces!)
build
- Build the functions applicaionserve
- Build the functions application in--watch
mode and start the Firebase Emulatorsdeploy
- Run the Firebase CLIdeploy
command with the application's Firebase configuration. This target accepts forwarded command line options.lint
- Lint the functions application codetest
- Run Jest unit tests on the functions application code
Once your Nx Firebase application has been initially generated you are free to change the firebase configurations however you like. If you don't use Firebase functions, you can choose to either simply ignore or not deploy them, or just remove the functions
settings from the Firebase configuration for your application.
The Firebase CLI usually warns you anyway if you try to deploy a feature that isn't yet enabled on your Firebase Project console.
This plugin supports multiple Firebase Applications/Projects inside one Nx workspace.
Each Nx Firebase Application generates its own firebase.<appname>.json
configuration in the Nx workspace root which can then be used with any Firebase CLI command by using the --config <config>
CLI option.
When using multiple Firebase projects in a workspace, remember that there is only one
.firebaserc
file to contain aliases for all of your deployment targets.You can add projects using
firebase use --add
as normalIt's fine to add multiple Firebase projects to your workspace
.firebaserc
file, but remember to correctly switch between them usingfirebase use <alias>
before any deployments!
If you only use a single Firebase project in your Nx workspace, feel free to delete the auto-generated empty firebase.json
config and rename the app specific firebase.appname.json
config to just firebase.json
.
The Firebase CLI will then just use this default configuration file instead, and in this scenario there's no need to pass the additional --config
CLI option.
Important: If you do rename your firebase configuration files, remember to update (or remove) any
--config
settings in your Nx-Firebase application'sserve
anddeploy
targets in yourworkspace.json
orangular.json
configuration file.
An Nx-Firebase app is essentially a Firebase functions directory (along with the few other configuration files as mentioned above). The main difference is that there isn't a directory called functions
which you may be used to from projects setup by the Firebase CLI; with Nx-Firebase your app directory IS your functions folder.
Inside the new apps/appname
directory you will find the following functions-specific files:
package.json
- The stub package file used when deploying firebase functionssrc
- Your Firebase functions code goes in here. You are free to structure your code however you like in this directorytsconfig.json
andtsconfig.app.json
as usual
Note: Firebase functions
tsconfig.app.json
is by default set to targetes2018
which is the maximum ES version for Node 10 engine. This override exists in case the roottsconfig.base.json
sets an ES target that is incompatible with Firebase Functions Node environment. If you are using Node 12 engine, you can change this target toes2019
.
You may find it convenient/familiar to create your Nx-Firebase application simply with functions
as it's app name eg. nx g @simondotm/nx-firebase:app functions
.
If you have multiple Firebase projects in your workspace, the Nx-Firebase application generator supports the --directory
option, so you may also find it convenient to organise your workspace as follows:
apps/<projectname>/...<projectapps>
And generate your app as follows:
nx g @simondotm/nx-firebase:app functions --directory projectname
So your workspace layout might look like this:
/apps
/project1
/functions
/web
/site1-app
/site2-app
/mobile
/app
/project2
/functions
/web
...
If you have one or more other web apps (Angular/React/HTML) that are deployed to a hosting site on your Firebase project, simply add them to your workspace as usual using the standard nx g
app generators.
Then just update your firebase.appname.json
hosting configuration to point to the dist/apps/<webapp>
where your web app build output is.
You can then run the Firebase CLI as usual to deploy the site:
firebase deploy --only hosting --config firebase.appname.json
Or
nx deploy appname --only hosting
If you deploy static websites to Firebase Hosting (that do not need to get built by Nx), just create a folder in your apps
directory (rather than generate an Nx web app) and put your content in that folder. Then update the hosting
section of your firebase.appname.json
to simply point directly to this folder.
The firebase CLI hosting deploy command above will just upload the static content as required.
An application generated by Nx-Firebase is by default configured to host content in the apps/appname/public
directory.
To keep the root of your Nx workspace tidy, the Nx-Firebase plugin puts default Firebase rules and index files inside the app folder, and the firebase.appname.json
configuration file simply points to them there.
Again, this works just fine with the usual Firebase CLI command, eg:
firebase deploy --only firestore:rules --config firebase.appname.json
Or
nx deploy appname --only firestore:rules
This is also useful for cleaner separation if you have multiple Firebase projects in your Nx workspace.
Again, you are free to modify these locations if you wish by simply changing the Firebase configuration files; the Nx-Firebase plugin does not use these configuration files in any way.
To bring an existing Firebase project into an Nx workspace, simply generate the Nx Firebase application(s) first, and then just overwrite the generated Firebase configuration & rules/indexes with your existing firebase.json
, rules & index files.
Nx-Firebase supports use of Nx Libraries within functions code.
To use a shared library with an Nx Firebase Application (Functions), first create a buildable (and it must be buildable) Typescript Nx node library in your workspace:
nx g @nrwl/node:lib mynodelib --buildable
You can now:
import { stuff } from '@myorg/mynodelib'
in your Firebase functions code as you'd normally expect.
You can then build your Firebase application (and any dependencies) with:
nx run appname:build --with-deps [--force]
This action will:
- Compile any dependent Typescript libraries imported by your Functions Application
- Compile the Typescript Functions code in your Firebase Application directory
- Auto generate dependencies in the output
package.json
for your functions - Make a local copy of all dependent node libraries referenced by your functions in the Firebase Application
dist/apps/appname/libs
directory and also in thedist/apps/appname/node_modules
directory - Update the Firebase functions
package.json
to use local package references to the dependent libraries.
TL;DR; version:
Building the Firebase Application takes care of all local library dependencies so you dont have to, and it ensures your functions are all ready to deploy simply using:
firebase deploy --only functions --config firebase.appname.json
Or
nx deploy appname --only functions
(see Technical Notes below for further explanation of all this)
Note: The Nx-Firebase plugin will detect if any non-buildable libraries have been imported by a firebase application, and halt compilation.
If you create Nx Libraries in subdirectories, you should use --importPath
when generating the buildable library, because @nrwl/node:lib
by default generates library path aliases that are incompatible with npm
package naming and also Firebase functions deployment. See Github discussion here.
For instance:
nx g @nrwl/node:lib nodelib --directory subdir --buildable
will generate Typescript path alias and package.json
name of @myorg/subdir/nodelib
(note the extra backslash separator) which isn't compatible with how Firebase functions are deployed.
Instead, when generating sub-directory Nx libraries that will be used by Firebase functions, use the --importPath
feature to ensure the library has a compatible package name. eg.
nx g @nrwl/node:lib nodelib --directory subdir --buildable --importPath='@myorg/subdir-nodelib'
Note: The Nx-Firebase plugin will detect if any such libraries are imported by a firebase application, and halt compilation.
Publishable vs Buildable Nx Node Libraries
As of Nx 12.3.4, there doesn't seem to be much difference between a --publishable
and a --buildable
node library. The docs suggest the builder for publishable libraries generates optimized/webpack code but this doesn't seem to be the case in practice.
Both options have @nrwl/node:package
as the builder target, and both generate a package.json
file for the library, but using --publishable
when generating a library will require that --importPath
is specified.
Both of these library options are compatible with Nx-Firebase applications.
Buildable Node libraries in Nx are typically used in environments where it is typical to export the entire public API for a library in a single index.ts
, and then consuming some or all of this API by importing from this barrel export eg.
import { stuff } from '@myorg/mylib'
For Firebase functions however, we need to be efficient with our imports since each import has some impact on the cold start time of our functions because:
- Importing an entire library from a barrel file is rarely optimal on a per-function basis
- We are not doing any tree-shaking of our imports, since we're not using webpack
- Buildable node libraries do not really support multiple entry points like Angular libraries do
So we ideally need a means of only importing the specific modules that our functions actually use from our libraries.
One option is to sub divide into multiple smaller libraries that are used by Firebase functions, however this can lead to too much fragmentation of shared code, and inconvenient splitting of library code in ways that prevent us from grouping libraries by functional scope.
A better solution for Firebase Functions is to use deep imports, that allow our functions to import only specific modules from node libraries, eg.
import { SpecificModule } from '@myorg/mylib
/src/lib/specificmodule
'
This approach means our buildable library is still packaged as @myorg/mylib
, but we are able to import its internal modules as well as its public API.
When we add buildable node libraries to our Nx workspace, the node library generator adds its own Typescript path alias to compilerOptions.paths
in our workspace tsconfig.base.json
such as:
"@myorg/mylib": ["libs/mylib/src/index.ts"]
This alias only allows node libraries or applications to import the library as import { module } from '@myorg/mylib'
(the main
entry point defined in our buildable library package.json
), but if we also add an additional wildcard path alias to our tsconfig.base.json
such as:
"@myorg/mylib/src/*": ["libs/mylib/src/*"]
our functions code and libraries are now able to import other public API modules from src
as:
import { MyModule } from '@myorg/mylib/src/test
(where test.ts
is an additional public API barrel file for the library that perhaps exports some subset of the library functionality)
Or, we might choose to import modules more directly as:
import { SomeModule } from '@myorg/mylib/src/lib/somemodule
(where SomeModule
is exported in /libs/mylib/src/lib/somemodule.ts
)
Note: This technique is a trade off between best practice and performance.
Normally we'd discourage exposing the private implementations of libraries as a best practice, but for Firebase functions, cold start times are a genuine (and sometimes significant) side effect of importing unnecessary dependencies, so by directly deep importing only specific module from libraries we can improve runtime performance of our functions at the cost of exposing our private library implementations.
Note also that the path aliases used must match the built library output directory structure in
dist
, because thepackage
executor for buildable@nrwl/node:lib
projects that import other buildable node libraries generate temporarytsconfig.generated.json
files for buildable node libraries , and automatically converts the Typescript compiler path aliases in this temp config to point to the builtdist/libs/...
library output instead of the library source.
The Firebase emulators work well within Nx and nx-firebase
.
To startup the Firebase emulator suite:
nx emulate appname
- will launch the Firebase emulators using the app's firebase configuration
When the Firebase Emulators are running, it is no longer possible to run nx build appname
because the build script always attempts to delete the output directory in dist
prior to compilation, which conflicts with a resource lock placed on this directory by the emulators.
To workaround this, use:
nx build appname --with-deps --deleteOutputPath=false
which will instruct the build to proceed without cleaning the output directory first. Note this approach could lead to scenarios where spurious files exist in dist
due to skipping the cleaning step.
The emulators automatically detect changes to configuration files and source files for functions.
To serve the application, use:
nx serve appname
Which will build the functions application and all of its dependencies, whilst launching the emulators, and the typescript compiler in watch mode.
IMPORTANT: Note that whilst
nx serve
will be useful when changing existing functions code, due totsc --watch
being enabled, it will NOT correctly detect changes if additional library imports are added to the source code or changes are made to any imported libraries during a watched session. To remedy this, relaunch the emulators by runningnx serve
again.
This plugin builds on a number of interesting conversations in the Nx Github issues regarding use of Firebase within Nx. At the time of writing, there were various workarounds and tooling solutions, which didn't quite hit the spot for me, hence I did some of my own experimentation to see what could be done.
The main feature I wanted was to be able to easily build & deploy Firebase functions in a way that would be familiar (Firebase CLI) but also leverage the power of Nx libraries to share common backend code across multiple Firebase projects.
Firebase is quite particular about how it deploys functions code to GCP, in so much that:
- It does not upload
node_modules
folder, and - It requires all code used by all functions to be self-contained within the
functions
directory that is set in thefirebase.json
configuration file.
An additional constraint is that we do not want to use Webpack for building function code because:
- It prevents us from using dynamic function exports that optimize cold starts (since webpack bundles all functions and all exports into one module)
- With Firebase functions, there's really no need or benefit to optimize, minify or otherwise change the compiled JS we upload
Other considerations:
- We do care about only deploying functions code that is relevant to the functions being deployed (some workarounds upload all of the workspace)
- The Firebase CLI is great and familar, so we don't really want to have to add any extra complexity or wrappers around it if we can help it.
Supporting Nx libraries as external packages with Firebase functions is slightly tricky. The solution the Nx-Firebase plugin uses is to use local package references.
Nx-Firebase has a custom build executor which is based on the @nrwl/node:package
executor, with a few additional build steps.
- It compiles any node libraries that are dependents of our Firebase application
- It compiles the Firebase application functions code as a pure buildable Typescript library package
- It auto-generates npm dependencies in the output
package.json
- It makes a copy of all dependent node libraries in
dist/apps/appname/libs/...
- It makes another copy of these libraries in
dist/apps/appname/node_modules/...
- It transforms any local library package dependencies in the output
package.json
to use local file references to the local./libs/...
folder eg.
"@myorg/mylib": "0.0.1" => "@myorg/mylib": "file:libs/mylib"
These extra steps ensure that the Firebase CLI can deploy functions with all of the correct dependencies and local libraries fully self contained within the dist/apps/appname
directory (which pleases the CLI).
The reason why we make an additional copy of dependent libraries to the node_modules
folder is because the Firebase CLI runs some checks (and partially processes the primary functions entry point node script) prior to deployment to ensure everything is in order.
There's no need to do a full npm install
here, we only have to ensure local libraries exist inside node_modules
at deployment time.
The Firebase CLI doesn't upload the node_modules
folder, only the ./src
and ./libs
folders.
Firebase functions does support private packages, but they are frankly a bit of a headache, so packaging libraries locally is a much neater solution, and actually works pretty nicely with Nx workspace workflow.
Project specific Typescript Paths
If you use ModuleAlias
and TSC path aliases in your Firebase functions (as I do), please note that I've not yet figured out how to support project-specific tsc
paths
in the Typescript configurations. It seems tricky since Nx uses path aliases in the workspace root tsconfig
for library imports, and adding any path overrides in applications will overwrite these. The best workaround I can think of is to add global aliases with some naming convention to the workspace root TSC config... If anyone has any better ideas they'd be very welcome!
Unit Tests
I've not implemented a full set of unit tests yet, but the e2e tests do perform a few standard tests.
Notes mainly for my own benefit/reminder here.
npx create-nx-plugin simondotm --pluginName nx-firebase
nx run nx-firebase:build
nx run nx-firebase-e2e:e2e
Creates a temporary workspace in /tmp/nx-e2e/
After the e2e test has completed, the plugin can be further manually tested in this temporary workspace.