Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow development of Firebase Cloud Functions #836

Closed
Bielik20 opened this issue Oct 20, 2018 · 62 comments
Closed

Allow development of Firebase Cloud Functions #836

Bielik20 opened this issue Oct 20, 2018 · 62 comments
Labels
community This is a good first issue for contributing outdated scope: misc Misc issues stale type: feature

Comments

@Bielik20
Copy link
Contributor

Cloud Functions are a great way of developing a micro-service based backend. Just like with node backend it is common to share types and some business logic between the two. Could we come up with some way of integrating them with an Nx Workspace? What are the things that people would like to see in that area?

Current Behavior

No easy way of integrating a Cloud Functions within an Nx Workspace.

Expected Behavior

Developers should be able to easily develop a Cloud Functions backend within an Nx Workspace.

@jmarbutt
Copy link

I am interested in this but I am not sure what all I would want on this to make it work easier.

@jmarbutt
Copy link

I would like to see it easier to build out the functions project while referencing libraries. Here are some other ideas:

  • Add Triggers for FireStore, Storage, Http, etc (with tests)
  • Emulate Firestore
  • Unit Testing Functions
  • Add Function to core index so it is exported correctly.

I think that is it off the top of my head.

@Bielik20
Copy link
Contributor Author

I think we could divide it into milestones:

  1. MVP
    1. Scaffold Cloud Functions project that uses the root package.json (does not create a new one)
    2. Cloud Functions can reference 'libs'
    3. Triggers
  2. Emulate Firestore / Local development
  3. Unit Testing Functions

Can we agree on that? Did I miss something?

@jmarbutt
Copy link

Yeah I think that is perfect

@Bielik20
Copy link
Contributor Author

@FrozenPandaz I think this feature has gained quite a popularity. I do realize that there may be other, more important tasks on hand, but we would like to get an update if there are even plans to take it on.

@Bielik20
Copy link
Contributor Author

Bielik20 commented Feb 6, 2019

I am not sure it is the right place to post it but... I have made a project with working firebase functions in nx workspace. It uses node-app as a base. You can find it here and here. I think it covers most of the milestones, apart from scaffolding obviously.

@nkwood
Copy link

nkwood commented Jun 25, 2019

If anyone is using firebase in a nx managed monorepo, you should be aware of this behavior: firebase/firebase-js-sdk#1696 (comment)

@jongood01
Copy link

@Bielik20 Setting "source": "/" in your firebase.json is not ideal at all as 'firebase deploy' will then try to upload all your source code. I've been banging my head against a wall try to get this to work. Ideally 'source' would be something like 'dist/apps/functions'.

@Bielik20
Copy link
Contributor Author

Bielik20 commented Jul 4, 2019

@jongood01 Yeah I agree but then firebase would complain that there is no npm package there:

Error: No npm package found in functions source directory. Please run 'npm init' inside dist/apps/functions

It is annoying but I think there is little harm in keeping source code up there. It cannot be accessed by anyone from the outside. Deployment is a little longer but also nothing one should worry about.

Or is there a specific reason against it you have in mind?

@jongood01
Copy link

@Bielik20 No other reason but on our team we run the deploy as part of a CI process and the mono repo is going to get huge with a lot of code that has nothing to do with the code on Firebase so builds will get slower and slower. I had a hack around to create a minimal package.json in the dist folder but this was also a hack around and not ideal either. Will pick one or the other option for now.
Not really the place for this but I'm really struggling to get Universal rendering working with this as well. I like your idea of having a lib to handle the express server part that gets imported into the cloud function but requiring the main server bundle throws errors.

@spy4x
Copy link

spy4x commented Jul 24, 2019

Am I right - this issue affects deployment of Nest.js app as well?
Thanks Nx team for all the efforts! Nx is awesome and I really appreciate your work :)

Btw, any movements on this issue?

@johannesnormannjensen
Copy link

johannesnormannjensen commented Jul 30, 2019

Hi @Bielik20 and @jongood01

I had also run into the problem of not wanting to upload all my source code. What i found is that you can configure functions to ignore certain directories in firebase.json like this:

...
"functions": {
      "predeploy": [
        "npm run lint functions",
        "npm run build functions --prod"
      ],
      "ignore": [
        "**/apps/web/**",
        "**/apps/web-e2e/**",
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "source": "/"
    },
...

Documentation here: https://firebase.google.com/docs/hosting/full-config

@vsavkin vsavkin added the scope: misc Misc issues label Dec 4, 2019
@george43g
Copy link

If nrwl could work natively with firebase cloud functions, that would be ideal.

I'm building an app with Firebase. The amount of hours I've spent fixing configuration errors and trying to get local emulation and testing to work is far higher than time spent doing actual programming. Nothing works, ever. I've already tried Lerna but it seems to be more-so designed for open source projects. Not to mention that its package hoisting breaks everything, meaning I need to npm install each package manually anyway, defeating the purpose.

I was hoping that nrwl would finally help in this regard. A setup that would allow me to start actually programming again rather than adjusting config files all day.

@spy4x
Copy link

spy4x commented Apr 7, 2020

Guys, I've found a workaround, that allows using single package.json for Nx monorepo with Firebase Functions.

TLDR: https://github.com/spy4x/nx-with-firebase-functions

Idea:

Firebase Functions need only 2 files to work:

  1. index.js (you could name it differently, like main.js) with your exported Functions
  2. package.json with next minimal template:
{
  "main": 'index.js',
  "dependencies": { ...your production dependencies here } 
}

So we can use @nrwl/node or @nrwl/nest or other node-based application, build it using $ nx build <appName> (bundle will appear in dist/apps/<appName>) and generate a new package.json file inside dist/apps/<appName> with dependencies that only needed for our application (for example, avoiding Angular/React application dependencies).

I created a simple JS script that gets a list of appName-specific dependencies from the package.json section that I called firebase-functions-dependencies.

And it works fine!

You can run $ firebase emulators:start and test app locally.

The only flaw of this workaround that I see now - it's necessary to manually update root package.json file's firebase-functions-dependencies section when you add a new dependency for your Firebase Functions application.

Check my demo repo (link above) for details. I split changes into commits, so you can see that there is no magic behind. The main change is in this commit: https://github.com/spy4x/nx-with-firebase-functions/commit/c95997976df1f985c2fce146708cb26ace3f5208

I hope that helps somebody. And maybe someone will find a way to update firebase-functions-dependencies automatically.

P.S. you can use any triggers, dependencies and frameworks (ie Nest.js) with this approach. Just don't forget to update firebase-functions-dependencies.

@beeman beeman mentioned this issue Apr 20, 2020
@JoelCode
Copy link
Contributor

Google Cloud Functions Generator

Generate a Google Cloud Function within a Nx workspace with dev tools:

  • Create : nx generate @joelcode/gcp-function:http functionName
  • Serve : nx serve functionName
  • Test : nx test functionName
  • Deploy : nx deploy functionName

I took Nx development strategy for front-end components & applied it to back-end microservices (Google Cloud Functions). With my plugin, I can create & deploy production-ready microservices in 5 minutes. I then combine my microservices to develop a business automation strategy, business analytics, or data streaming pipeline.

GitHub Repository

@FrozenPandaz
Copy link
Collaborator

@spy4x That's amazing! Thank you for sharing!

We have started to provide the ability for the community to provide plugins for Nx and I think this would be a great candidate. If @spy4x or somebody else would be interested in maintaining a plugin for Nx + Firebase functions integrations, we would be happy to help and promote it!

@FrozenPandaz FrozenPandaz added the community This is a good first issue for contributing label May 24, 2020
@jaytavares
Copy link
Contributor

My take on @spy4x's solution above. I added the depcheck package as a dev dependency and use it to automate the discovery of unused dependencies within the functions project and then remove those from the generated package.json file.

This means that there's no more need for a separate firebase-functions-dependencies section in your package.json file! 🎉

const packageJson = require('../../package.json') // Take root package.json
const path = require('path')
const fs = require('fs').promises
const depcheck = require('depcheck')

const ROOT_PATH = path.resolve(__dirname + '/../..')
const distProjectPath = `${ROOT_PATH}/dist/apps/functions`

console.log('Creating cloud functions package.json file...')

let packageJsonStub = {
  engines: { node: '10' },
  main: 'main.js',
}

depcheck(
  distProjectPath,
  {
    package: {
      dependencies: packageJson.dependencies,
    },
  },
  unused => {
    let dependencies = packageJson.dependencies
    if (unused.dependencies.length > 0)
      console.log('Deleting unused dependencies:')
    unused.dependencies.reduce((acc, dep, i) => {
      console.log(`${i + 1} - Deleting ${dep}`)
      delete acc[dep]
      return acc
    }, dependencies)

    fs.mkdir(path.dirname(distProjectPath), { recursive: true }).then(() => {
      fs.writeFile(
        `${distProjectPath}/package.json`,
        JSON.stringify({
          ...packageJsonStub,
          dependencies,
        })
      )
        .then(() =>
          console.log(`${distProjectPath}/package.json written successfully.`)
        )
        .catch(e => console.error(e))
    })
  }
)

@vdjurdjevic
Copy link

@jaytavares Great job! I have one more improvement for this setup. Instead of custom scripts in package.json, we could add builders with @nrwl/workspace:run-commands. That way it feels more integrated with nx, and also enables usage of affected --target=command. For example we could add command for generating package.json, edit serve command to run emulator, and add deploy command.

{
    "pkgJson": {
          "builder": "@nrwl/workspace:run-commands",
	  "options": {
		"command": "ts-node scripts/build-firebase-package-json.ts",
		"cwd": "tools"
	  }
    },
    "serve": {
	"builder": "@nrwl/workspace:run-commands",
	"options": {
		"command": "firebase emulators:start --only functions --inspect-functions"
	}
    },
    "shell": {
	"builder": "@nrwl/workspace:run-commands",
	"options": {
		"command": "firebase functions:shell --inspect-functions"
	}
    },
    "deploy": {
	"builder": "@nrwl/workspace:run-commands",
	"options": {
		"command": "firebase deploy"
	}
    }
}

@jaytavares
Copy link
Contributor

@vdjurdjevic 👍 That's the same approach I used to be able to use nx. Though, I renamed and wrapped the node builder in order to just tack the tool script run at the end of nx build.

"architect": {
  "build-node": {
    "builder": "@nrwl/node:build",
    "options": { },
    "configurations": { }
  },
  "build": {
    "builder": "@nrwl/workspace:run-commands",
    "options": {
      "commands": [
        {
          "command": "nx run functions:build-node"
        },
        {
          "command": "node tools/scripts/build-cloud-functions-package-file.js"
        }
      ],
      "parallel": false
    },

@nartc
Copy link
Contributor

nartc commented Jul 21, 2020

@vdjurdjevic @jaytavares Thanks for the snippets. I was able to get Firebase Functions up and running with NestJS within a Nx workspace. However, I need to run build first before I can even serve with firebase emulators and there's no live reload at all (aka watch). Were you guys able to get --watch working for firebase emulators?

PS: Not sure if it's worth mentioning but initially I have source: "/" in firebase.json but that didn't work because firebase emulators keeps saying: Cannot find module /path/to/my_workspace_name

@vdjurdjevic
Copy link

Watch works. Just run nx build functions(or your project name) --watch, and in the separate terminal run emulators. You should have live reload. At least works with my setup. I used node project instead of nextjs, but that should not be an issue.

@vdjurdjevic
Copy link

vdjurdjevic commented Jul 22, 2020

I opened an issue in firebase repo to support a more flexible project structure for functions. That would enable better integration with Nx, and I would be willing to contribute full-featured plugin for firebase (not just functions, but firestore config, hosting, storage, etc..). @FrozenPandaz can you please check it out and tell me what you think?

@dalepo
Copy link

dalepo commented Aug 6, 2020

I created an angular workspace and then I wrapped my functions under a node app but I'm struggling to use imported libraries since I'm getting this error when trying to deploy firebase functions:

Error: Cannot find module '@nrwl/workspace'
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:636:15)
at Function.Module._load (internal/modules/cjs/loader.js:562:25)
at Module.require (internal/modules/cjs/loader.js:692:17)
at require (internal/modules/cjs/helpers.js:25:18)
at Object. (/workspace/decorate-angular-cli.js:28:20)
at Module._compile (internal/modules/cjs/loader.js:778:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
at Module.load (internal/modules/cjs/loader.js:653:32)
at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
at Function.Module._load (internal/modules/cjs/loader.js:585:3)

Anyone faced this? Any help is appreciated.

@ildemartinez
Copy link

Hello

I have the same issue.... any news about that?

imagen

@dalepo
Copy link

dalepo commented Aug 9, 2020

I found out that this is related to the postinstall script with the angular-cli decorator (decorate-angular-cli.js)

One quick way to make it work is removing the postinstall script from package.json before deploying to functions, then add it back again so the nx client doesn't break. Is there a way to skip the postinstall script when deploying to firebase functions?

@beeman
Copy link
Contributor

beeman commented Aug 9, 2020

I found out that this is related to the postinstall script with the angular-cli decorator (decorate-angular-cli.js)

One quick way to make it work is removing the postinstall script from package.json before deploying to functions, then add it back again so the nx client doesn't break. Is there a way to skip the postinstall script when deploying to firebase functions?

You're probably looking for the --ignore-scripts command:

The --ignore-scripts argument will cause npm to not execute any scripts defined in the package.json. Source here.

@markwhitfeld
Copy link

markwhitfeld commented Jan 4, 2021

Just to add an additional tweak to @dobromyslov's awesome comment:

The angular.json "build" configuration needs the configurations section in order to support the --prod flag and to pass it on to the "build-node" configuration.

Note: I was also having an issue where it was saying 'ts-node' is not recognized as an internal or external command (even though it was installed as a dependency), so I also added the npx call to the command that uses this.

        "build": {
          "builder": "@nrwl/workspace:run-commands",
          "options": {
            "commands": [
              {
                "command": "nx run functions:build-node"
              },
              {
                "command": "npx ts-node tools/scripts/build-firebase-functions-package-json.ts"
              }
            ],
            "parallel": false
          },
          "configurations": {
            "production": {
              "prod": true
            }
          }
        }

Specifying the --prod flag will allow for a more optimised build of the function and to support the production environment configuration that was supplied in the example.

@EthanSK
Copy link

EthanSK commented Jan 6, 2021

There is this community plugin, not sure how well it works: https://github.com/JoelCode/gcp-function

@dobromyslov
Copy link

dobromyslov commented Jan 16, 2021

@markwhitfeld thank you for the addition. I also did that but did not post here.

@EthanSK

There is this community plugin, not sure how well it works: https://github.com/JoelCode/gcp-function

gcp-function plugin does not provide full flexibility as such as you develop a standalone Firebase project. Standard Firebase approach lets you mix multiple functions as well as mix various triggers (e.g. firestore document change, storage file write, scheduler) and create a full featured data processing pipeline.

@dobromyslov
Copy link

Does anybody know how to properly create and manage 2 or more Firebase projects under one NX workspace?

I am struggling one drawback of @spy4x @jaytavares @vdjurdjevic approach: can't split dependencies between functions. I added one function which requires puppetteer package. As you may know this dependency is rather heavy and takes much time during functions deployment.

My idea concludes in dependencies separation between functions or even better between several Firebase projects:

  • First project is for functions using puppeteer.
  • Second project is for other light-weight functions.

@dobromyslov
Copy link

dobromyslov commented Jan 16, 2021

Found a way to create, run local emulators and deploy two or more NX apps with Firebase support using firebase --config firebase.second-app.json (this feature was added in July, 2020 firebase/firebase-tools#1115)

Just add second NX application with Firebase support as described above: #836 (comment) and add another firebase.second-app.json file to the root folder:

Expand firebase.second-app.json
{
  "functions": {
    "source": "dist/apps/second-app"
  },
  "emulators": {
    "functions": {
      "port": 5011
    },
    "firestore": {
      "port": 8091
    },
    "hosting": {
      "port": 5010
    },
    "pubsub": {
      "port": 8095
    },
    "ui": {
      "enabled": true
    },
    "auth": {
      "port": 9199
    }
  }
}

And use --config firebase.second-app.json parameter in all your angular.json like this:

"serve": {
  "builder": "@nrwl/workspace:run-commands",
  "options": {
    "command": "nx run second-app:build && env-cmd firebase --config firebase.second-app.json emulators:start --only functions --inspect-functions"
   }
},

@johanchouquet
Copy link

Found a way to create, run local emulators and deploy two or more NX apps with Firebase support using firebase --config firebase.second-app.json (this feature was added in July, 2020 firebase/firebase-tools#1115)

Just add second NX application with Firebase support as described above: #836 (comment) and add another firebase.second-app.json file to the root folder:

Expand firebase.second-app.json
And use --config firebase.second-app.json parameter in all your angular.json like this:

"serve": {
  "builder": "@nrwl/workspace:run-commands",
  "options": {
    "command": "nx run second-app:build && env-cmd firebase --config firebase.second-app.json emulators:start --only functions --inspect-functions"
   }
},

I think this is the proper way of doing things within a monorepo & cloud functions. Firebase CFs are powerfull, but in monorepos, we have quite a lot of code. Only one node application (i.e. functions) for these very versatile needs is not possible.

So, I see functions as a node application, integrated within Nx workspace, with an angular.json file. It gives a lot of flexibility to develop / test / deploy the right CFs for the right project we have in the monorepo, which uses a specific Firebase environment.
The ability to specify which firebase.json file to take into account is a game changer. I'll try in the near future to do just what i described.

What are your best pratices guys ?

@IainAdamsLabs
Copy link

Now that NX node build supports generatePackageJson (https://nx.dev/latest/angular/node/build#generatepackagejson) does this negate the need for the separate build-firebase-functions-package-json script?

@jaytavares
Copy link
Contributor

jaytavares commented Mar 4, 2021

Now that NX node build supports generatePackageJson (https://nx.dev/latest/angular/node/build#generatepackagejson) does this negate the need for the separate build-firebase-functions-package-json script?

Yes, that's what it means for me at least. I was just able to test the generatePackageJson functionality and it works great. My setup used my script above which used the depcheck package in order to remove unneeded dependencies from the resulting package.json file. I also had renamed my "build" builder to "build-node" and called it from another "build" builder which ran my custom script afterward. The new setting obviates all that and works great. The package.json file is generated and includes only the dependencies that you actually import in your functions code. I was able to add the additional elements needed for cloud functions projects by adding the following package.json file to my functions application:

{
  "engines": {
    "node": "12"
  },
  "main": "main.js"
}

Big 👍 to the Nx team for their implementation of the generatePackageJson feature.

@dobromyslov
Copy link

@IainAdamsLabs thank you for the hint. Seems like this tool simplifies boilerplate.
But it still lacks another feature - generate package-lock.json. This is optional but it increases deployment speed of Google Cloud Functions. To do it I run additional command for each Cloud Functions project in the workspace like this: cd dist/apps/functions && npm install --package-lock-only and then deploy to the cloud.

@shelooks16
Copy link

Am I the one here who doesn't see the provided solution with webpack good enough?

Firebase Functions processed with webpack potentially have bad outcome. With only one function it's good, but with more functions the webpack solution doesn't seem pretty sweet. At least the provided solution doesn't fit my use case.

  • Since webpack jumbles up the code in the output file, this can potentially break some runtime functionality.
    For instance, I received The default Firebase app does not exist. Make sure you call initializeApp() before using any of the Firebase services. Although in the entry file initializeApp() is called before any function export, the error is still present. And the reason is that webpack output places initializeApp() somewhere below admin sdk calls such as admin.firestore()
  • Global scope is shared across all functions
  • No possibility to use "automatic function exports" as described here: https://codeburst.io/organizing-your-firebase-cloud-functions-67dc17b3b0da

I rolled up a fully custom solution which uses just typescript compiler (no webpack) with opportunity to import shared local libraries.

Final functions folder structure for apps/functions/src:

.
└── src
    ├── firestore
    │   ├── profile
    │   │   ├── onCreate.f.ts
    │   │   └── onDelete.f.ts
    │   └── chat
    │       ├── onUpdate.f.ts
    │       └── messages
    │           └── onCreate.f.ts
    ├── pathAliases.ts
    ├── sharedLibs.ts
    └── main.ts

Firestore structure:

.
├── profile
│   └── {uid}
└── chat
    └── {chatId}
        └── messages
            └── {msgId}

Sample firestore/profile/onCreate.f.ts:

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import { User } from '@WORKSPACE/shared/types';

const firestore = admin.firestore();

export default functions
  .firestore.document('profile/{profileId}')
  .onCreate(async (snapshot, context) => {
    // ...
    // const u: User = { ... };
  }

As you may notice, folder structure reflects Firestore structure. All functions to be deployed are suffixed with .f.ts. They are automatically picked-up and exported from main.ts.

With given profile collection in Firestore, profile/onCreate.f.ts and profile/onDelete.f.ts are functions that run for profile collection documents. The same logic applies to subcollections with given folder/file structure: chat/messages/onCreate.f.ts - the function will run when a document inside messages subcollections is created.

Deployed functions names will reflect camelCased folder structure. Deployed firestore/profile/onCreate.f.ts will be named profileOnCreate, while firestore/chat/messages/onCreate.f.ts => chatMessagesOnCreate

With below files you will be able to achieve that structure with NX + have an ability to import local libs + use tsc compiler instead of webpack.

Source code files

apps/functions/src/main.ts

Expand
import './pathAliases';
import * as admin from 'firebase-admin';
import glob from 'glob';
import camelCase from 'camelcase';
import { resolve } from 'path';

admin.initializeApp();

// add more folders for function detection if needed
const lookupFolders = ['firestore'];
const baseFoldersRegex = `(${lookupFolders.join('|')})`;

const EXTENSION = '.f.js';
const files = glob.sync(`./+${baseFoldersRegex}/**/*${EXTENSION}`, {
  cwd: __dirname
});

for (let f = 0, fl = files.length; f < fl; f++) {
  const file = files[f];

  const functionName = camelCase(
    file
      .split(EXTENSION)
      .join('')
      .split('/')
      .join('_')
      .replace(new RegExp(baseFoldersRegex), '')
  );

  if (
    !process.env.FUNCTION_NAME ||
    process.env.FUNCTION_NAME === functionName
  ) {
    // eslint-disable-next-line
    const mod = require(resolve(__dirname, file));
    exports[functionName] = mod.default || mod;
  }
}

apps/functions/src/pathAliases.ts

Expand
import ModuleAlias from 'module-alias';
import path from 'path';
import { getAliasesForSharedLibs } from './sharedLibs';

// paths in tsconfig.base.ts will let typescript recognize the paths
// paths specified and registerd by module-alias will register paths for
// compiled javascript

const paths = {
  ...getAliasesForSharedLibs()
};

const aliases = Object.entries(paths).reduce((acc, [alias, paths]) => {
  const aliasCorrected = alias.replace('/*', '');

  const pp = paths[0].replace('/*', '/');
  const pathCorrected = path
    .join(__dirname, pp)
    .replace(/\\/g, '/')
    .replace('.ts', '.js');

  return {
    ...acc,
    [aliasCorrected]: pathCorrected
  };
}, {});

ModuleAlias.addAliases(aliases);

apps/functions/src/sharedLibs.ts

Expand
export const sharedLibsConfig = {
  /**
   * List of all libs in the monorepo that functions import
   */
  libs: ['shared/types'],
  /**
   * Path to a folder in which output all shared libraries
   *
   * @note Folder is relative to functions root folder
   */
  outDir: 'libs'
};

export function getAliasesForSharedLibs(): Record<string, string[]> {
  const WORKSPACE_NAME = 'PUT_YOUR_WORKSPACE_NAME_HERE';
  const { libs, outDir } = sharedLibsConfig;

  return libs.reduce((result, libName) => {
    const alias = `@${WORKSPACE_NAME}/${libName}`;
    const paths = [`../${outDir}/${libName}/src/index.js`];

    return { ...result, [alias]: paths };
  }, {});
}

You have to only adjust sharedLibs.ts to your workspace:

  1. Change WORKSPACE_NAME to yours
  2. Change sharedLibsConfig.libs to list all local libs, and dependencies of libs that functions import. With given libs/shared/types, you have to include shared/types.

apps/functions/tsconfig.app.json

Expand
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "../../dist",
    "sourceMap": false,
    "module": "commonjs",
    "types": ["node"],
    "strict": true,
    "removeComments": true,
    "esModuleInterop": true
  },
  "exclude": ["**/*.spec.ts"],
  "include": ["**/*.ts"]
}

Make sure that tsconfig.app.json has outDir set to "../../dist"

Workspace configuration

firebase.json

Expand
{
  "functions": {
    "source": "dist/apps/functions",
    "predeploy": ["nx run functions:build"],
    "runtime": "nodejs10"
  }
}

tools/scripts/copy-shared-libs-for-functions.ts

Expand
import * as path from 'path';
import * as fs from 'fs-extra';

async function main() {
  const args = process.argv.slice(2);
  if (!args?.length || !args[0]) {
    throw new Error('Application name must be provided.');
  }

  const APP_NAME = args[0];
  const CFG_FILE = 'sharedLibs.js'; // matches (functions/src/sharedLibs.ts)

  const ROOT_PATH = path.resolve(__dirname + '/../..');
  const DIST_FOLDER = `${ROOT_PATH}/dist`;

  const { sharedLibsConfig } = await import(
    `${DIST_FOLDER}/apps/${APP_NAME}/src/${CFG_FILE}`
  );

  if (!sharedLibsConfig?.libs || !sharedLibsConfig?.outDir) {
    throw new Error('Config for copying shared libs has wrong format.');
  }

  const { libs, outDir } = sharedLibsConfig;

  console.log('Copying shared libraries:', libs);
  console.log('Total:', libs.length);
  console.log(
    'Destination:',
    `${DIST_FOLDER}/apps/${APP_NAME}/${sharedLibsConfig.outDir}`
  );

  const promises = libs.map(async (libName: string) => {
    const src = `${DIST_FOLDER}/libs/${libName}`;
    const dest = path.join(DIST_FOLDER, `apps/${APP_NAME}`, outDir, libName);

    await fs.copy(src, dest, {
      overwrite: true
    });
  });

  await Promise.all(promises);
}

main()
  .then(() => {
    // Nothing to do
  })
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

tools/scripts/build-firebase-functions-package-json.ts

Expand
import * as path from 'path';
import * as depcheck from 'depcheck';
import * as fs from 'fs';

const PACKAGE_JSON_TEMPLATE = {
  engines: { node: '10' },
  main: 'src/main.js'
};

async function main(): Promise<void> {
  const args = process.argv.slice(2);
  if (!args?.length || !args[0]) {
    throw new Error('Application name must be provided.');
  }

  const APPLICATION_NAME = args[0];

  /*****************************************************************************
   * package.json
   * - Filter unused dependencies.
   * - Write custom package.json to the dist directory.
   ****************************************************************************/
  const ROOT_PATH = path.resolve(__dirname + '/../..');
  const DIST_PROJECT_PATH = `${ROOT_PATH}/dist/apps/${APPLICATION_NAME}`;

  const packageJson = require('../../package.json');
  console.log('Creating cloud functions package.json file...');

  // Get unused dependencies
  const { dependencies: unusedDependencies } = await depcheck(
    DIST_PROJECT_PATH,
    {
      package: {
        dependencies: packageJson.dependencies
      }
    }
  );

  // Filter dependencies
  const requiredDependencies = Object.entries(
    packageJson.dependencies as { [key: string]: string }
  )
    ?.filter(([key, value]) => !unusedDependencies?.includes(key))
    ?.reduce<{ [key: string]: string }>((previousValue, [key, value]) => {
      previousValue[key] = value;
      return previousValue;
    }, {});

  console.log(
    `Required dependencies count: ${
      Object.values(requiredDependencies)?.length
    }`
  );

  // Write custom package.json to the dist directory
  await fs.promises.mkdir(path.dirname(DIST_PROJECT_PATH), { recursive: true });
  await fs.promises.writeFile(
    `${DIST_PROJECT_PATH}/package.json`,
    JSON.stringify(
      {
        ...PACKAGE_JSON_TEMPLATE,
        dependencies: requiredDependencies
      },
      undefined,
      2
    )
  );

  console.log(`Written successfully: ${DIST_PROJECT_PATH}/package.json`);
}

main()
  .then(() => {
    // Nothing to do
  })
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Root package.json

Expand
  "dependencies": {
    "camelcase": "^6.2.0",
    "fs-extra": "^9.1.0",
    "glob": "^7.1.6",
    "module-alias": "^2.2.2",
    // ...
  },
  "devDependencies": {
    "@types/fs-extra": "^9.0.7",
    "@types/glob": "^7.1.3",
    "@types/module-alias": "^2.0.0",
    "copyfiles": "^2.4.1",
    "depcheck": "^1.4.0",
    "onchange": "^7.1.0",
    "rimraf": "^3.0.2",
    "ts-node": "~9.1.1",
    "typescript": "4.0.2",
    "wait-on": "^5.2.1",
    // ...
  },

workspace.json

Expand
    "functions": {
      "root": "apps/functions",
      "sourceRoot": "apps/functions/src",
      "projectType": "application",
      "prefix": "functions",
      "targets": {
        "compile": {
          "executor": "@nrwl/workspace:run-commands",
          "options": {
            "commands": [
              "npx tsc -p apps/functions/tsconfig.app.json",
              "npx ts-node tools/scripts/copy-shared-libs-for-functions.ts functions",
              "npx ts-node tools/scripts/build-firebase-functions-package-json.ts functions"
            ],
            "parallel": false
          }
        },
        "build": {
          "executor": "@nrwl/workspace:run-commands",
          "options": {
            "commands": [
              "npx rimraf dist/apps/functions",
              "nx compile functions",
              "cd dist/apps/functions && npm install --package-lock-only"
            ],
            "parallel": false
          }
        },
        "compile-dev-assets": {
          "executor": "@nrwl/workspace:run-commands",
          "options": {
            "commands": [
              "npx onchange -ik libs/**/*.ts -- npx ts-node tools/scripts/copy-shared-libs-for-functions.ts functions",
              "npx ts-node tools/scripts/build-firebase-functions-package-json.ts functions",
              "npx copyfiles apps/functions/.runtimeconfig.json dist"
            ]
          }
        },
        "serve": {
          "executor": "@nrwl/workspace:run-commands",
          "options": {
            "commands": [
              "npx tsc -p apps/functions/tsconfig.app.json -w",
              "npx wait-on dist/apps/functions && nx compile-dev-assets functions",
              "npx wait-on dist/apps/functions/package.json && firebase serve --only functions"
            ]
          }
        },
        "shell": {
          "executor": "@nrwl/workspace:run-commands",
          "options": {
            "command": "nx build-dev functions && firebase functions:shell --inspect-functions"
          }
        },
        "download-runtimeconfig": {
          "executor": "@nrwl/workspace:run-commands",
          "options": {
            "commands": [
              "firebase functions:config:get > apps/functions/.runtimeconfig.json"
            ]
          }
        },
        "logs": {
          "executor": "@nrwl/workspace:run-commands",
          "options": {
            "command": "firebase functions:log"
          }
        },
        "lint": {
          "executor": "@nrwl/linter:eslint",
          "options": {
            "lintFilePatterns": ["apps/functions/**/*.ts"]
          }
        },
        "test": {
          "executor": "@nrwl/jest:jest",
          "outputs": ["coverage/apps/functions"],
          "options": {
            "jestConfig": "apps/functions/jest.config.js",
            "passWithNoTests": true
          }
        }
      }
    },

workspace.json for functions became pretty sophisticated. Here is the description of key targets:

  • compile. Compiles functions and imported local libs with typescript + copies compiled local libs to compiled functions folder + builds package.json
  • compile-dev-assets. Watches for any changes in libs and copies new changes to compiled functions folder + copies local .runtimeconfig.json + builds package.json
  • serve. Compiles functions in watch mode + runs compile-dev-assets target + runs firebase serve.
  • build. Builds the code for production/deployment. Runs compile target + creates package-lock.json

How this works

With given workspace structure:

.
├── apps
│   ├── functions
│   └── webapp
└── libs
    └── shared
        └── types

let's assume we have to share types between functions and webapp (i.e. nextjs):

import { User } from '@WORKSPACE/shared/types';

For webapp no need for any action.

For functions to import shared libs, you will have to add them to sharedLibsConfig.libs inside apps/functions/src/sharedLibs.ts:

// sharedLibs.ts
export const sharedLibsConfig = {
  libs: ['shared/types'], // <-- here
  outDir: 'libs'
};

That's it. After that running nx build functions or nx serve functions will compile functions with shared libs :)

If you look inside the dist/apps/functions folder, the output will be:

.
└── functions
    ├── src
    └── libs
        └── shared
            └── types
  1. Libs specified in sharedLibs.ts will be copied over to compiled functions folder under libs.
  2. Because of the module-alias package and pathAliases.ts, compiled javascript will recognize paths correctly during runtime.
  3. Created folder libs in functions matches the name in sharedLibsConfig.outDir. You can basically put any name there.

The power of module-alias and just copying files around is a very strong combo :)

So far this solution has been working great for me. I hope this will be useful for somebody too!

@paulbijancoch
Copy link

A "externalDependencies": "none" in the "build"-"architect"-section for the App (in the angular.json) did the trick for me 🚀

@dobromyslov
Copy link

@IainAdamsLabs @jaytavares I tested @nrwl/node:build with generatePackageJson with NestJS. Everything builds very well, emulator runs smooth but when deploy a function to the cloud it throws errors caused by absent packages:

Detailed stack trace: Error: Cannot find module 'passport'
Require stack:
- /workspace/node_modules/@nestjs/passport/dist/auth.guard.js
...

Then I compared package.json generated by @nrwl/node:build with generatePackageJson and by build-firebase-functions-package-json.ts and found out that native NRWL generator skips packages required by NestJS.

@github-actions
Copy link

This issue has been automatically marked as stale because it hasn't had any recent activity. It will be closed in 14 days if no further activity occurs.
If we missed this issue please reply to keep it active.
Thanks for being a part of the Nx community! 🙏

@KingDarBoja
Copy link

KingDarBoja commented Apr 22, 2021

What is the advantage of setting all of this without webpack (@shelooks16) compared to this alternative: https://itnext.io/nx-nest-firebase-the-dream-616e8ee71920

Also the pros of getting rid of webpack to achieve "automatic function exports" is a big plus to me but I am not sure if the NestJS alternative does that as well.

@shelooks16
Copy link

@KingDarBoja Hi!

I do not use Nest.js. And this is not what was initially desired. What about other function types, triggers, pubsub? Nest solution creates one function of type onRequest to serve the API. It is good but serves completely different purpose.

The solution I posted above allows to develop functions in the same way as without NX, but at the same it allows to share libs. And of course "automatic function exports" is yum 😃

And to be honest, I think Nest.js is not that good for serverless environments since it has a pretty good amount of boot time which increases the cold start.

@maeri
Copy link

maeri commented May 11, 2021

@shelooks16 would you mind sharing project structure ? thx a lot :)

@shelooks16
Copy link

@maeri Hi. Yeah, sure :)

At very basic extent, we have something like this (all 3 apps share types, dates, currency libs). We did not split functions app itself into smaller libs:

.
├── apps
│   ├── reactapp
│   ├── nodeapp
│   └── functions
│       ├── src
│       │   ├── auth
│       │   ├── firestore
│       │   ├── onRequest
│       │   ├── pubsub
│       │   ├── config
│       │   ├── main.ts
│       │   ├── pathAliases.ts
│       │   └── sharedLibs.ts
│       └── .runtimeconfig.example.json
├── libs
│   └── shared
│       ├── types
│       ├── dates
│       └── currency
├── tools
│   └── scripts
│       ├── build-firebase-functions-package-json.ts
│       ├── copy-shared-libs-for-functions.ts
│       └── start-emulators.js
├── firebase.json
└── workspace.json
// sharedLibs.ts 

export const sharedLibsConfig = {
  /**
   * List of all libs in the monorepo that functions import
   */
  libs: ['shared/types', 'shared/dates', 'shared/currency'],
  /**
   * Path to a folder in which put all shared libraries
   *
   * @important Must be relative to functions root folder (i.e. '/shared')
   */
  outDir: '/libs'
};

There is also a separate script that starts firebase emulators (if needed). Above solution uses the outdated way, here is what we do right now:

//  start-emulators.js

const { spawn, exec } = require('child_process');

const printCallback = (err, stdout, stderr) => {
  console.log(stdout);
  if (stderr) {
    console.log('[STD_ERR]', stderr);
  }
  if (err) {
    console.log('[ERR]', err);
  }
};

function releasePorts(c) {
  exec('npx kill-port 4000 8080 9099 5001 9000 8085', c);
}

function main() {
  releasePorts(printCallback);
  const emulatorsProcess = spawn(
    'npx wait-on dist/apps/functions/package.json && firebase emulators:start --only functions,auth,firestore,pubsub --import=./.emulatordb --export-on-exit=./.emulatordb',
    {
      shell: true,
      stdio: 'inherit'
    }
  );

  [
    `exit`,
    `SIGINT`,
    `SIGUSR1`,
    `SIGUSR2`,
    `uncaughtException`,
    `SIGTERM`
  ].forEach((eventType) => {
    process.on(eventType, () => {
      releasePorts();
      emulatorsProcess && emulatorsProcess.kill();
    });
  });
}

main();

@simondotm
Copy link
Contributor

simondotm commented May 12, 2021

Hi folks, I too was looking for a Firebase solution for my own use with Nx, and was following this thread with interest, so I decided to have a bash at a custom plugin, and after a couple of weeks figuring out how plugins (and Nx) work , I have published a first version to npm.

If anyone wants to give it a try out I'd be glad to hear if its useful to others (I'm currently using it in my own monorepo).

My approach was to try and integrate Firebase workflow within Nx in only a lightly opinionated way that gives us the ability to use Nx libraries in firebase functions code but still be able to use the firebase CLI and also other Firebase features (hosting, rules, indexes, etc.) within Nx without any fuss.

The plugin uses a custom builder to compile functions code as a plain Typescript library, and then do all of the post-build work to auto-generate dependency outputs in dist that are ready to deploy.

Nx Library support works by copying build output for each dependent Nx library to the output folder and referencing them in the functions package.json as local modules.

https://www.npmjs.com/package/@simondotm/nx-firebase

@KingDarBoja
Copy link

KingDarBoja commented May 14, 2021

I did a small try with that plugin locally in some experimental repo and seems like that's what everyone been looking for.

Followed the steps and got into the build part and the output api lib does seems to retain the structure of automatic function exports, although I haven't tested sharing libs between this firebase app and other libs.

Looking for others to give it a try, awesome work @simondotm ❤️

EDIT Well, I am happy with the output result, seems like grouped exports are kept by using TSC and importing node libs does what is mentioned in the readme.

App Folder Structure
example-nxfirebase-input

Output Structure
example-nxfirebase-output

Build log

$ nx build coljobs-api --with-deps

>  NX  Running target build for project coljobs-api and 1 task(s) that it depends on.


———————————————————————————————————————————————

> nx run coljobs-api:build [retrieved from cache]
Compiling TypeScript files for project "coljobs-api"...
Done compiling TypeScript files for project "coljobs-api".
Copying asset files...
Done copying asset files.
- Processing dependencies for firebase functions app
 - Firebase functions app has 'npm' dependency 'firebase-functions'
 - Firebase functions app has 'lib' dependency '@ngfire-showcase/company/domain'
- Updating firebase package.json

———————————————————————————————————————————————

>  NX   SUCCESS  Running target "build" succeeded

  Nx read the output from cache instead of running the command for 2 out of 2 tasks.

@simondotm
Copy link
Contributor

Hello again folks, I've just released v0.3.0 of my plugin, which now has some improvements and fixes, as well as support for deploy and serve targets. The project also got renamed, so any existing users - please uninstall the old plugin and reinstall the new plugin. Should work with Nx 12.1.1 onwards, but the latest Nx 12.3.4 is recommended for --watch support to work.

@ghost
Copy link

ghost commented Jul 31, 2021

Firebase is getting more popular by the day. It certainly deserves to be a first-class Nx workspace citizen. It's a shame that the Nx team decided to outsource it to community plugins.

@Bielik20
Copy link
Contributor Author

@paxforce As much as we would all love to have first class support for firebase, we need to remember that it adds a lot of maintenance work on Nx team. I feel like it is better for them to focus on improving Nx itself and community tools. There is tremendous amount of use cases for monorepo. Multiple languages, frameworks, tools. I kind of agree that from their perspective it is better to outsource that work to the community, since for example they may not have a firebase expert on hand to create that implementation.

@ghost
Copy link

ghost commented Oct 3, 2021

@shelooks16 Is this structure even suitable for Angular 12 with Nx Workspace?

@shelooks16
Copy link

@shelooks16 Is this structure even suitable for Angular 12 with Nx Workspace?

Yeah, why not? Structure of one NX app does not affect other NX apps. The suggested functions approach is complicated but it works. If you need help with setting it up, I will be glad to help :)

As an alternative, checkout this package which achieves similar result (posted above):
https://www.npmjs.com/package/@simondotm/nx-firebase

@github-actions
Copy link

This issue has been closed for more than 30 days. If this issue is still occuring, please open a new issue with more recent context.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 23, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
community This is a good first issue for contributing outdated scope: misc Misc issues stale type: feature
Projects
None yet
Development

No branches or pull requests