Skip to content

Instantly share code, notes, and snippets.

@theprojectsomething
Last active November 7, 2019 09:34
Show Gist options
  • Save theprojectsomething/2076f856f9c4488366dc88e6e8ab2f20 to your computer and use it in GitHub Desktop.
Save theprojectsomething/2076f856f9c4488366dc88e6e8ab2f20 to your computer and use it in GitHub Desktop.
Firebase Cloud Functions: Multi-file setup

Firebase Cloud Functions: Multi-file setup

Create grouped cloud functions automagically from an appropriately named file structure, while retaining the ability to selectively deploy to the cloud.

Take the following structure as an example:

functions
-node_modules
-utils
-endpoints
 -user
  -create.js
  -update.js
  -friend
   -add.js
   -remove.js
  -admin
   -list.js
 -debug
  -user.js
  -ignore
   -system.js

It would create this export structure:

exports = {
  user: {
    create(),
    update(),
    friend: { add(), remove() }
    admin: { list() }
  }
  debug: { user() }
}

Serve these functions locally:

✔  functions: user.create: http://localhost:5000/{project}/{region}/user-create
✔  functions: user.update: http://localhost:5000/{project}/{region}/user-update
✔  functions: user.friend.add: http://localhost:5000/{project}/{region}/user-friend-add
✔  functions: user.friend.remove: http://localhost:5000/{project}/{region}/user-friend-remove
✔  functions: user.admin.list: http://localhost:5000/{project}/{region}/user-admin-list
✔  functions: debug.user: http://localhost:5000/{project}/{region}/debug-user

And deploy these functions to the server:

✔  Function URL (user-create): https://{region}-{project}.cloudfunctions.net/user-create
✔  Function URL (user-update): https://{region}-{project}.cloudfunctions.net/user-update
✔  Function URL (user-friend-add): https://{region}-{project}.cloudfunctions.net/user-friend-add
✔  Function URL (user-friend-remove): https://{region}-{project}.cloudfunctions.net/user-friend-remove

The following structure would do the same thing (dots.not.dirs):

functions
-node_modules
-utils
-endpoints
 -user.create.js
 -user.update.js
 -user.friend.add.js
 -user.friend.remove.js
 -user.admin.list.js
 -debug.user.js
 -debug.ignore.system.js

I prefer the second dot-notation approach as it makes require('./../utils') easier than nesting.

Note the available naming modifiers which can appear in any part of the path; either as a directory name, or separated by dot notation anywhere in a file name:

Modifier shorthand description
admin a admin functions do not deploy and are only available when emulating locally
debug d debug functions do not deploy and are only available when emulating locally
ignore i ignored scripts can sit within the folder structure without being parsed

Finally if you were to selectively firebase deploy --only function:users.friend you would get:

✔  Function URL (user-friend-add): https://{region}-{project}.cloudfunctions.net/user-friend-add
✔  Function URL (user-friend-remove): https://{region}-{project}.cloudfunctions.net/user-friend-remove

Enjoy!

P.S. the script may require Node 10+ to run as-is. To target the correct runtime update the engines field in your package.json:

"engines": {"node": "10"}
const functions = require('firebase-functions');
const path = require('path');
const glob = require('glob');
const ENDPOINT_FOLDER = './endpoints';
const DO_NOT_DEPLOY = /^(admin|a|debug|d)$/;
const IGNORE = /^(ignore|i)$/;
const BREAK_ON_ERROR = true;
const is = {
emulating: process.env.hasOwnProperty('FIREBASE_PROJECT'),
deploying: !process.env.hasOwnProperty('FUNCTION_NAME'),
};
let skipped = [];
glob.sync(`./**/*.js`, {
cwd: path.resolve(__dirname, ENDPOINT_FOLDER),
})
.map(file => ({
path: file.slice(2),
components: file.slice(2, -3).split(/[\/.]/g),
}))
.sort((a, b) => b.components.length - a.components.length)
.forEach(file => {
// ignore by name
if (file.components.find(c => IGNORE.test(c))) return;
// firebase naming standard
const FB_NAME = file.components.join('-');
// function is currently being triggered
is.triggered = process.env.FUNCTION_NAME === FB_NAME;
// only deploy files locally or if allowed to deploy
is.deployable = is.emulating || !file.components.find(c => DO_NOT_DEPLOY.test(c));
// export module if triggered or deploying
if (is.triggered || is.deploying && is.deployable) {
// map the module to a deep path: { [component]: { [component]: module } }
file.components.reduce((_, c, i, list) => {
// get the map for each path component
if (i < list.length - 1) {
if (!_[c]) _[c] = {};
return _[c];
}
// skip files where a naming conflict exists with a directory
if (_[c]) return skipped.push(`./${file.path.slice(0, -3)}`);
// export the module
_[c] = require(`./${ENDPOINT_FOLDER}/${file.path}`);
}, exports);
}
});
// don't allow conflicts to deploy
if (BREAK_ON_ERROR && skipped.length) {
throw new Error(`naming conflict: "${skipped.join('", "')}"`);
}
@dfdgsdfg
Copy link

dfdgsdfg commented Nov 7, 2019

If you have deploy error like below. I think index.js to uses some unsupported api which is stable node version 8 on functions.

⚠  functions[db-call(asia-northeast1)]: Deployment error.
Function failed on loading user code. Error message: Node.js module defined by file index.js is expected to export function named db.call.onSomething

Change this in your package.json

  "engines": {
    "node": "10"
  }

Reference:
https://firebase.google.com/docs/functions/manage-functions?#set_runtime_options

@theprojectsomething
Copy link
Author

@dfdgsdfg thank you - didn't think to mention which version of node this was targeting!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment