Skip to content

Plugin for Nx that adds support for deploying Cloud Functions for Firebase.

License

Notifications You must be signed in to change notification settings

snorreks/nx-cloud-functions-deployer

Repository files navigation

nx-cloud-functions-deployer

npm (nx-cloud-functions-deployer)

This is a plugin for Nx that adds support for deploying Cloud Functions for Firebase.

From version 2.0.0 this plugin only supports cloud functions v2, if you want v1 support use version 1.2.2.

Features

  • Auto alias support
  • Support for multiple environments
  • Esbuild for faster builds
  • Detect changes and only deploy changed functions
  • No longer export all functions in a index.ts file, but deploy each function individually for smaller bundles
  • Configurable deploy options
  • Deploy with Node 14/16/18/20 and esm
  • Cloud functions v2 support
  • Deploy rules and indexes
  • Execute scripts locally
  • Read env file and copy to clipboard
  • Cloud cache support
  • Run emulators locally
  • AWS SAM: build, deploy and watch logs for lambda functions

Install

pnpm i -D nx-cloud-functions-deployer

Description

This plugin adds a deploy executor that will build and deploy your cloud functions.

It uses esbuild to bundle your functions and then uses the firebase-tools to deploy them.

Prerequisites

  • You will need to have the firebase-tools installed. Either globally or locally in your project. If you install it globally you have to set packageManager option to global.
pnpm i -D firebase-tools

If you want to use Cloud cache or run scripts you will also need tsx installed.

pnpm i -D tsx

Helper Functions

You need to import the helper functions from nx-cloud-functions-deployer. This will allow you to configure your functions and make the functions stronger typed.

Schedule Example

For schedule functions options.schedule is required.

import { onSchedule } from 'nx-cloud-functions-deployer';

export default onSchedule(
	(context) => {
		console.log('daily', context);
	},
	{
		schedule: 'every day 00:00',
	},
);

Auth Example

Auth triggers have the following functions: onAuthCreate, onAuthDeleted,beforeAuthCreate and beforeAuthDeleted.

import { onAuthCreate } from 'nx-cloud-functions-deployer';

export default onAuthCreate(({ uid }) => {
	console.log('New user created: uid', uid);
});

Database Example

Database triggers have the following functions: onValueCreated, onValueDeleted, onValueUpdated and onValueWritten. You must always add the ref for each database function.

import { onValueCreated } from 'nx-cloud-functions-deployer';

export default onValueCreated(
	({ data }) => {
		console.log('New gmail user created:', data.val());
	},
	{
		ref: '/user/{data.key=*@gmail.com}',
	},
);

Firestore Example

When you use the onWritten, onCreated, onUpdated or onDeleted helper functions for firestore. The data will automatically convert the snapshot to

{
 ...documentSnapshot.data(),
 id: documentSnapshot.id,
}
// shared lib
import type { CoreData } from 'nx-cloud-functions-deployer';
export interface UserData extends CoreData {
	email: string;
}
import { onCall } from 'nx-cloud-functions-deployer';
import type { UserData } from '@my-project/shared';

export default onCreated<UserData>(({ data }) => {
	console.log(`User ${data.email} created`);
});

Https Example

onCall and onRequest can give even better typing with frontend. By importing a interface from a shared file.

// shared lib
export type MyFunctions = {
	my_function_name: [
		{
			message: string; // request data
		},
		{
			response: boolean; // response data
		},
	];
};
import { onCall } from 'nx-cloud-functions-deployer';
import type { MyFunctions } from '@my-project/shared';

export default onCall<MyFunctions, 'my_function_name'>(({ data }) => {
	console.log(data); // { message: string }
	return { response: true };
});

Assets and external dependencies

To include assets and external dependencies you can use the assets and external options.

External will install the dependencies in the function folder and add them to the package.json. This is useful for dependencies that do not work with bundle.

Assets will copy the files from src/assets to the dist folder (right next to index.js)

import { onCall } from 'nx-cloud-functions-deployer';

export default onCall(
	({ data }) => {
		return { response: true };
	},
	{
		assets: ['test.png'],
		external: ['sharp'],
		keepNames: true,
	},
);

Also note that if you use sharp, you need keepNames: true. See documentation

Limitations

You cannot have comments, variables or numeric separators in the options section. If the options section is invalid it will skip the options and deploy as default.

All these lines in the options are not allowed:

import { onCall } from 'nx-cloud-functions-deployer';

const region = 'europe-west1';
const memory = '2GB';

export default onCall(
	(data, context) => {
		return { response: true };
	},
	{
		region,
		timeoutSeconds: 1_800,
		memory: memory,
		// a random comment
	},
);

Folder Structure

See the example for a better understanding of the folder structure.

It is recommend to have the following folder structure, but it is not required.

├── your-nx-project
│   ├──src
│   │  ├── controllers
│   │  │  ├── https
│   │  │  │  ├── callable
│   │  │  │  ├── request
│   │  │  ├── database
│   │  │  ├── firestore
│   │  │  ├── storage
|   |  |  ├── auth
│   │  │  ├── pubsub
│   │  │  │  ├── schedule
│   │  │  │  ├── topic

The folders in controllers will different deployment types:

The default function names will be the path from the api/callable/database/scheduler folder to the file. For example, the function controllers/api/stripe/webhook_endpoint.ts will be deployed as stripe_webhook_endpoint.

Firestore Structure

The firestore structure is a little different. It is recommend the folder structure to match to structure in the firestore. For example, if you have a firestore structure like this:

├── users # collection
│   ├── uid # a user document
│   │  ├── notifications # a sub collection
│   │  │  ├── notificationId # a notification document in the sub collection

Then you would have the following folder structure:

├── firestore
│  ├── users
│  │  ├── [uid]
│  │  │  ├── created.ts # will be called every time a user document is created.
│  │  │  ├── updated.ts # will be called every time a user document is updated.
│  │  │  ├── deleted.ts # will be called every time a user document is deleted.
│  │  │  ├── notifications
│  │  │  │  ├── [notificationId]
│  │  │  │  │  ├── created.ts # will be called every time a notification document is created.

The default function name for database/firestore functions will omit [id]. Example: controllers/database/users/[id]/created.ts will be deployed as users_created.

Custom Structure

To customize the folder structure, change the functionsDirectory in options. If you change the structure you have to specify the document for firestore functions and ref for database functions.

Example:

// controllers/firestore/my-custom-user-created-file.ts
import { onCreated } from 'nx-cloud-functions-deployer';
import type { UserData } from '@my-project/shared';

export default onCreated<UserData>(
	({ data }) => {
		console.log(`User ${data.email} created`);
	},
	{
		functionName: 'custom_function_name',
		document: 'users/{id}',
	},
);

Cloud cache

The plugin will detect changes on the deployed functions locally. But it is also possible to cache the changes for the deployed functions on your own server. To do this create a file in the project directory called functions-cache.ts. The file needs to export two function fetch and update which will be called by the plugin. Note you will also get the environments, so you can use process.env, if you want to hide production secrets for fetching/updating the cache.

import type {
	FunctionsCacheFetch,
	FunctionsCacheUpdate,
} from 'nx-cloud-functions-deployer';

export const fetch: FunctionsCacheFetch = async ({ flavor }) => {
	// fetch the cache from the cloud
};

export const update: FunctionsCacheUpdate = async ({
	flavor,
	newFunctionsCache,
}) => {
	// update the cache in the cloud
};

See the example for how to setup cloud cache with jsonbin

Logger

If you want to see metric for each function (like opentelemetry or sentry) , add a logger.ts file in the src folder (see example). The logger will be build and added for each function.

Executors

deploy

...
 "targets": {
  "deploy": {
   "executor": "nx-cloud-functions-deployer:deploy",
   "options": {
    "flavors": {
     "development": "firebase-project-development-id",
     "production": "firebase-project-production-id"
    }
   }
  },

deploy options

Option Description Default Alias
flavors A object of the flavors to use, the key is the flavor name and value is the firebase project id. required
flavor The flavor to use, default will be the first key in the flavors object
production If true, the flavor will be 'production' if flavor is not defined false prod
development If true, the flavor will be 'development' if flavor is not defined false dev
outputDirectory The output directory. dist/<relative-path-to-project> outDir
envFiles the key is the flavor name and value is path to the env file, default is .env.${flavor}
tsconfig The tsconfig file to use for the build in the project directory. tsconfig.json tsconfig
region The default region to deploy the functions, if it is not set in the deploy file. See Cloud Functions Locations. us-central1 location
silent Whether to suppress all logs. false s
verbose Whether to run the command with verbose logging. false v
concurrency The number of functions to deploy in parallel 5 c
envString Stringify version of the environment. This is useful when you want to deploy using CI/CD. undefined ciEnv
only Only deploy the given function names separated by comma undefined o
force Force deploy all functions, even if no files changed false f
packageManager The package manager to use for deploying with firebase-tools. Either: pnpm, npm, yarn or global. pnpm pm
dryRun If true, then it will only build the function and not deploy them. false d, dry
functionsDirectory Relative path from the project root to the functions directory. src/controllers inputDirectory
nodeVersion The node version to use for the functions. 20 node
minify Whether to minify the functions true

deploy examples

pnpm nx deploy functions --flavor production
pnpm nx deploy functions --development
pnpm nx deploy functions --development --only my_function,my_other_function --f
# will deploy only the functions my_function and my_other_function
# and deploy them even if no files have changed

script

The plugin also provide support to run scripts locally. The plugin will run any files in the scripts directory. The files needs to export default a function.

import { firestore } from '$configs/firestore';
import type { ScriptFunction } from 'nx-cloud-functions-deployer';

export default (async ({ prompt }) => {
	console.log('prompt', prompt);
	const { uid } = await prompt<{ uid: string }>({
		type: 'input',
		name: 'uid',
		message: 'Enter the uid',
		validate: (value) => {
			if (value.length === 20) {
				return true;
			}

			return 'The uid must be 20 characters long';
		},
	});

	const documentSnap = await firestore.collection('users').doc(uid).get();

	if (!documentSnap.exists) {
		throw new Error('User not found');
	}
	return { id: documentSnap.id, ...documentSnap.data() };
}) satisfies ScriptFunction;

You can also add a script-config.{flavor}.ts file in the project root. In these files you can execute code before before running a script.

To make firebase work locally see example.

...
 "targets": {
  "script": {
   "executor": "nx-cloud-functions-deployer:script",
   "options": {
    "flavors": {
     "development": "firebase-project-development-id",
     "production": "firebase-project-production-id"
    }
   }
  },

script options

Option Description Default Alias
flavors A object of the flavors to use, the key is the flavor name and value is the firebase project id. required
flavor The flavor to use, default will be the first key in the flavors object
production If true, the flavor will be 'production' if not defined false prod
development If true, the flavor will be 'development' if not defined false dev
envFiles the key is the flavor name and value is path to the env file, default is .env.${flavor}
tsconfig The tsconfig file to use for the script in the project directory. tsconfig.json tsconfig
silent Whether to suppress all logs. false s
verbose Whether to run the command with verbose logging. false v
scriptsRoot Relative path from the project root to the scripts directory. scripts
runPrevious Rerun the last executed script. false p
script The name of the script to run. If not set, it will prompt you to select from a list. undefined file

script examples

pnpm nx script functions --production
pnpm nx script functions --flavor development
pnpm nx script functions --development -p
# will run the last executed script in development

delete

The plugin provide support to delete unused function that are not in the project anymore. The plugin will delete any functions that are not in the functions directory.

...
 "targets": {
  "delete-unused": {
   "executor": "nx-cloud-functions-deployer:delete",
   "options": {
    "flavors": {
     "development": "firebase-project-development-id",
     "production": "firebase-project-production-id"
    }
   }
  },

delete options

Option Description Default Alias
flavors A object of the flavors to use, the key is the flavor name and value is the firebase project id. required
flavor The flavor to use, default will be the first key in the flavors object
production If true, the flavor will be 'production' if not defined false prod
development If true, the flavor will be 'development' if not defined false dev
envFiles the key is the flavor name and value is path to the env file, default is .env.${flavor}
tsconfig The tsconfig file to use for the script in the project directory. tsconfig.json tsconfig
silent Whether to suppress all logs. false s
verbose Whether to run the command with verbose logging. false v
deleteAll Whether to delete all functions even if they are in your project. false

emulate

The plugin provide support to emulate functions locally. The plugin will emulate any functions that are in the functions directory.

...
 "targets": {
  "emulate": {
   "executor": "nx-cloud-functions-deployer:emulate",
   "options": {
    "flavors": {
      "development": "firebase-project-development-id",
      "production": "firebase-project-production-id"
    },
 "only": ["functions"],
 "packageManager": "global",
 "minify": false
   }
  },

emulate options

Option Description Default Alias
flavors A object of the flavors to use, the key is the flavor name and value is the firebase project id. required
flavor The flavor to use, default will be the first key in the flavors object
envFiles the key is the flavor name and value is path to the env file, default is .env.${flavor}
tsconfig The tsconfig file to use for the script in the project directory. tsconfig.json tsconfig
silent Whether to suppress all logs. false s
verbose Whether to run the command with verbose logging. false v
only Only emulate the given services separated by comma "auth", "functions","firestore","hosting","pubsub","storage" undefined o
minify Whether to minify the functions true

read-env

This will read your .env file in your selected flavor, copy it to the clipboard and print it in console. This is so you can use envString when you deploy your functions in CI.

...
 "targets": {
  "delete-unused": {
   "executor": "nx-cloud-functions-deployer:read-env",
   "options": {
    "flavors": {
     "development": "firebase-project-development-id",
     "production": "firebase-project-production-id"
    }
   }
  },

read-env options

Option Description Default Alias
flavors A object of the flavors to use, the key is the flavor name and value is the firebase project id. required
flavor The flavor to use, default will be the first key in the flavors object
envFiles the key is the flavor name and value is path to the env file, default is .env.${flavor}
silent Whether to suppress all logs. false s
verbose Whether to run the command with verbose logging. false v

rules

See the example here.

sam

There is now also support for aws sam to deploy and watch logs. You need the SAM CLI installed to use this feature. See the example here.

...
  "deploy": {
   "executor": "nx-cloud-functions-deployer:sam-deploy",
   "options": {
    "flavors": {
     "development": "example-dev-test"
    },
    "bucket": "test"
   }
  },
  "logs": {
   "executor": "nx-cloud-functions-deployer:sam-logs",
   "options": {
    "flavors": {
     "development": "example-dev-test"
    },
    "name": "ExampleFunction",
    "tail": true
   }
  }