{"id":19620618,"url":"https://github.com/george43g/better-firebase-functions","last_synced_at":"2026-04-02T21:35:32.855Z","repository":{"id":36469537,"uuid":"226609579","full_name":"george43g/better-firebase-functions","owner":"george43g","description":"This repo provides functionality for a better way of organising files, imports and function triggers in Firebase Cloud Functions","archived":false,"fork":false,"pushed_at":"2024-05-10T09:12:52.000Z","size":2967,"stargazers_count":181,"open_issues_count":16,"forks_count":16,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-03-29T03:08:53.203Z","etag":null,"topics":["cloud-functions","firebase","firebase-cloud","firebase-cloud-functions","optimization"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/george43g.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-12-08T03:36:22.000Z","updated_at":"2024-11-18T02:16:13.000Z","dependencies_parsed_at":"2024-05-10T10:27:00.496Z","dependency_job_id":"8ae07fe3-b192-4495-a719-d582cf87182e","html_url":"https://github.com/george43g/better-firebase-functions","commit_stats":{"total_commits":146,"total_committers":5,"mean_commits":29.2,"dds":0.08904109589041098,"last_synced_commit":"e3476973ba46927bd88f2196a448e7fb46a1f894"},"previous_names":["gramstr/better-firebase-functions"],"tags_count":33,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/george43g%2Fbetter-firebase-functions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/george43g%2Fbetter-firebase-functions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/george43g%2Fbetter-firebase-functions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/george43g%2Fbetter-firebase-functions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/george43g","download_url":"https://codeload.github.com/george43g/better-firebase-functions/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247284952,"owners_count":20913704,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloud-functions","firebase","firebase-cloud","firebase-cloud-functions","optimization"],"created_at":"2024-11-11T11:19:28.133Z","updated_at":"2026-04-02T21:35:32.848Z","avatar_url":"https://github.com/george43g.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# better-firebase-functions\n\n**Auto-export Firebase Cloud Functions with cold-start optimization built in.**\n\n[![npm](https://img.shields.io/npm/v/better-firebase-functions)](https://www.npmjs.com/package/better-firebase-functions)\n[![CI](https://github.com/george43g/better-firebase-functions/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/george43g/better-firebase-functions/actions/workflows/npm-publish.yml)\n[![license](https://img.shields.io/npm/l/better-firebase-functions)](./LICENSE)\n\n---\n\n## What is this?\n\n`better-firebase-functions` replaces the boilerplate of manually importing and exporting every trigger with a single call. More importantly, it implements a **cold-start optimization**: at runtime, only the single function module that is actually being invoked is loaded — not every function in your project.\n\nIn large projects with many functions this can reduce cold-start time and memory usage significantly. In single-function projects it adds zero overhead.\n\nSupports:\n- **Gen 1** functions (`FUNCTION_NAME`)\n- **Gen 2** functions / Cloud Run (`K_SERVICE`, `FUNCTION_TARGET`)\n- **CJS and ESM** entry points\n- **esbuild, webpack, and Rollup** for per-function bundling (optional, takes the optimization further)\n\n---\n\n## Packages\n\n| Package | Description |\n|---|---|\n| [`better-firebase-functions`](./packages/core) | Core runtime library. Zero dependencies. |\n| [`better-firebase-functions-esbuild`](./packages/esbuild) | esbuild build helper for per-function bundles |\n| [`better-firebase-functions-webpack`](./packages/webpack) | webpack plugin for per-function bundles |\n| [`better-firebase-functions-rollup`](./packages/rollup) | Rollup plugin for per-function bundles |\n\n---\n\n## Quick Start\n\n### 1. Install\n\n```sh\nnpm install better-firebase-functions\n```\n\n### 2. Replace your entry point\n\n```typescript\n// src/index.ts\nimport { exportFunctions } from 'better-firebase-functions';\n\nexportFunctions({\n  __filename,\n  exports,\n  functionDirectoryPath: './functions',\n  searchGlob: '**/*.func.js',\n});\n```\n\n\u003e **Note on `searchGlob`:** the glob should match files as they exist **at runtime after compilation** (i.e. `.js` files). If you run TypeScript natively (via `tsx`, `ts-node`, etc.) you can glob `.ts` directly. Bundler plugins automatically expand `.js` globs to include `.ts` source files at build time.\n\n### 3. Write each trigger as a default export\n\n```typescript\n// src/functions/http/get-user.func.ts\nimport { onRequest } from 'firebase-functions/v2/https';\n\nexport default onRequest(async (req, res) =\u003e {\n  res.json({ ok: true });\n});\n```\n\n```typescript\n// src/functions/auth/on-create.func.ts\nimport { auth } from 'firebase-functions';\n\nexport default auth.user().onCreate(async (user) =\u003e {\n  // ...\n});\n```\n\nFunctions are named automatically from their file paths relative to `functionDirectoryPath`:\n\n| File | Exported as |\n|---|---|\n| `functions/on-create.func.ts` | `onCreate` |\n| `functions/auth/on-create.func.ts` | `auth-onCreate` |\n| `functions/http/api/get-users.func.ts` | `http-api-getUsers` |\n\nDashes in the name create **Firebase function groups**, so `firebase deploy --only functions:auth` works out of the box.\n\n---\n\n## How the Cold-Start Optimization Works\n\nWithout BFF, your entry point imports every function module at startup. In a project with 50 functions, all 50 modules are loaded, their dependencies resolved, and their closures formed on every cold start — even though only one function is being invoked.\n\nWith BFF, the entry point checks the runtime environment (`FUNCTION_TARGET`, `FUNCTION_NAME`, `K_SERVICE`) to identify which function is running. It then loads **only that module**. The other 49 are skipped entirely.\n\n```\nWithout BFF:  load module A + B + C + D + ... + N  (all N modules)\nWith BFF:     load module A only\n```\n\nDuring **deployment** (when no function-instance env var is set), BFF loads all modules so Firebase CLI can discover every trigger. This is the only time all modules are loaded.\n\nThe optimization is **purely subtractive** — it can only help, never hurt.\n\n---\n\n## API Reference\n\n### `exportFunctions(config)` — synchronous, CJS\n\n```typescript\nimport { exportFunctions } from 'better-firebase-functions';\n\nexportFunctions({\n  __filename,           // required — Node's __filename\n  exports,              // required — Node's exports / module.exports\n\n  // Discovery — all optional, defaults shown:\n  functionDirectoryPath: './',                     // relative to __dirname / entry point dir\n  searchGlob: '**/*.{js,ts}',                     // glob matching trigger files at runtime\n  funcNameFromRelPath: funcNameFromRelPathDefault, // custom name generator\n  __dirname: undefined,                            // override base dir (derived from __filename)\n\n  // Module loading — optional:\n  extractTrigger: (mod) =\u003e mod?.default,           // extract trigger from loaded module\n\n  // Logging — optional:\n  enableLogger: false,                             // enable performance timing logs\n  logger: console,                                 // custom logger object\n\n  // Build tools — optional:\n  exportPathMode: false,                           // export file paths instead of triggers (debug)\n});\n```\n\nReturns the populated `exports` object (also mutated in-place).\n\n### `exportFunctionsAsync(config)` — async, ESM-compatible\n\nIdentical config shape. Uses dynamic `import()` instead of `require()`. Use this for ESM function files or from an ESM entry point.\n\n```typescript\n// ESM entry point (e.g. index.mjs or package.json \"type\": \"module\")\nimport { exportFunctionsAsync } from 'better-firebase-functions';\n\nconst fns = await exportFunctionsAsync({\n  __filename: import.meta.filename,\n  exports: {},\n  functionDirectoryPath: './functions',\n  searchGlob: '**/*.func.js',\n});\n\nexport default fns;\n```\n\n### `discoverFunctionPaths(config)` — build-time discovery helper\n\nReturns structured discovery metadata for bundler plugins. Most users do not call this directly.\n\n```typescript\nimport { discoverFunctionPaths } from 'better-firebase-functions';\n\nconst discovery = discoverFunctionPaths({\n  __filename: entryPointPath,\n  functionDirectoryPath: './functions',\n  searchGlob: '**/*.func.js',\n});\n\n// discovery.entries: Record\u003cfuncName, { absPath, sourceRelativePath, runtimeRelativePath, outputRelativePath, outputEntryName }\u003e\n```\n\n---\n\n## Configuration Reference\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `__filename` | `string` | — | **Required.** Node's `__filename` (or `import.meta.filename` in ESM) |\n| `exports` | `object` | — | **Required.** Node's `exports` / `module.exports` |\n| `functionDirectoryPath` | `string` | `'./'` | Directory containing function files, relative to entry point |\n| `searchGlob` | `string` | `'**/*.{js,ts}'` | Glob pattern matching trigger files at runtime |\n| `funcNameFromRelPath` | `function` | built-in | Custom function name generator — `(relPath: string) =\u003e string` |\n| `extractTrigger` | `function` | `(mod) =\u003e mod?.default` | Extract trigger from loaded module |\n| `__dirname` | `string` | derived from `__filename` | Override discovery base directory |\n| `enableLogger` | `boolean` | `false` | Print performance timing logs |\n| `logger` | `object` | `console` | Custom logger with `time`, `timeEnd`, `log` methods |\n| `exportPathMode` | `boolean` | `false` | Export file paths instead of triggers (debugging / build tools) |\n\n---\n\n## Bundler Plugins\n\nBundler plugins take the optimization further: they produce **one independently bundled, tree-shaken file per function**. On cold start, Node.js parses only the code that specific function needs — no dead code from unrelated functions.\n\n### The single-source-of-truth design\n\nThe plugins execute your BFF entry point in build-discovery mode (`BFF_BUILD_DISCOVERY=1`) to reuse the exact same `functionDirectoryPath`, `searchGlob`, and `funcNameFromRelPath` already configured for runtime. You write your BFF config once, in the entry point. The bundler inherits it automatically.\n\nYour runtime `searchGlob` can target `.js` files — the plugins automatically expand it to match `.ts` source files at build time.\n\n### Output layout\n\nBundled outputs preserve the `functionDirectoryPath` and mirror the runtime file layout:\n\n```\nsrc/functions/auth/on-create.func.ts  →  dist/functions/auth/on-create.func.js\nsrc/functions/http/get-user.func.ts   →  dist/functions/http/get-user.func.js\n```\n\nThe deployed `main.js` uses the same `functionDirectoryPath` and `searchGlob` — no mismatch between build and runtime.\n\n### esbuild\n\n```sh\nnpm install -D better-firebase-functions-esbuild esbuild\n```\n\n```typescript\nimport { buildFunctions } from 'better-firebase-functions-esbuild';\nimport { resolve } from 'path';\n\nawait buildFunctions({\n  entryPoint: resolve(__dirname, 'src/index.ts'),\n  outdir: resolve(__dirname, 'dist'),\n  target: 'node20',\n});\n```\n\n→ Full docs: [`packages/esbuild/README.md`](./packages/esbuild/README.md)\n\n### webpack\n\n```sh\nnpm install -D better-firebase-functions-webpack webpack\n```\n\n```typescript\n// webpack.config.ts\nimport { BffWebpackPlugin } from 'better-firebase-functions-webpack';\n\nexport default {\n  target: 'node',\n  entry: 'src/index.ts',\n  plugins: [\n    new BffWebpackPlugin({\n      entryPoint: resolve(__dirname, 'src/index.ts'),\n    }),\n  ],\n};\n```\n\n→ Full docs: [`packages/webpack/README.md`](./packages/webpack/README.md)\n\n### Rollup\n\n```sh\nnpm install -D better-firebase-functions-rollup rollup\n```\n\n```typescript\n// rollup.config.ts\nimport { bffRollupPlugin, bffRollupOutput } from 'better-firebase-functions-rollup';\n\nexport default {\n  input: 'src/index.ts',\n  output: bffRollupOutput({ dir: 'dist' }),\n  plugins: [bffRollupPlugin({ entryPoint: resolve(__dirname, 'src/index.ts') })],\n};\n```\n\n→ Full docs: [`packages/rollup/README.md`](./packages/rollup/README.md)\n\n---\n\n## Common Patterns\n\n### Custom file suffix convention\n\n```typescript\nexportFunctions({\n  __filename,\n  exports,\n  searchGlob: '**/*.trigger.js', // only files ending in .trigger.js\n});\n```\n\n### Custom function name generator\n\n```typescript\nimport { exportFunctions } from 'better-firebase-functions';\nimport { basename } from 'path';\n\nexportFunctions({\n  __filename,\n  exports,\n  // Flat names — no group prefix — all functions at top level\n  funcNameFromRelPath: (relPath) =\u003e basename(relPath).replace(/\\.(func\\.)?(js|ts)$/, ''),\n});\n```\n\n### Named export instead of default\n\n```typescript\nexportFunctions({\n  __filename,\n  exports,\n  extractTrigger: (mod) =\u003e mod?.handler ?? mod?.default,\n});\n```\n\n### ESM entry point with top-level await\n\n```typescript\n// index.mjs\nimport { exportFunctionsAsync } from 'better-firebase-functions';\n\nexport default await exportFunctionsAsync({\n  __filename: import.meta.filename,\n  exports: {},\n  functionDirectoryPath: './functions',\n  searchGlob: '**/*.func.js',\n});\n```\n\n### Co-located test files — keep them out\n\nUse a specific glob pattern that excludes test files:\n\n```typescript\nexportFunctions({\n  __filename,\n  exports,\n  searchGlob: '**/*.func.js', // .test.js and .spec.js are not matched\n});\n```\n\n---\n\n## Environment Variables (Read by BFF)\n\n| Variable | Source | Priority | Notes |\n|---|---|---|---|\n| `FUNCTION_TARGET` | Functions Framework (Gen 2) | 1st | Most precise — exact registered function name |\n| `FUNCTION_NAME` | Firebase Gen 1, some Gen 2 | 2nd | May be a full resource path — last segment extracted |\n| `K_SERVICE` | Cloud Run (Gen 2) | 3rd | Lowercased by Cloud Run — canonicalized before matching |\n| `BFF_BUILD_DISCOVERY` | Bundler plugins | — | Set to `1` during build-time discovery; skips loading modules |\n\nWhen none of the function-identity variables are set, BFF is in deployment mode and loads all modules.\n\n---\n\n## Troubleshooting\n\n**Functions are not discovered (empty exports)**\n\n1. Check your `searchGlob` matches the compiled files. If you use `tsc`, globs for `.ts` won't find anything at runtime — use `.js`.\n2. Set `enableLogger: true` to see which files the glob finds at startup.\n3. Check `functionDirectoryPath` is correct relative to your entry point.\n\n**`Function 'x' is not defined in the provided module` on Gen 2**\n\nBFF's name matching failed. The most common causes:\n- The derived function name does not match what Cloud Run lowercases as `K_SERVICE`.\n- You use a custom `funcNameFromRelPath` whose output doesn't canonicalize to the Cloud Run service name.\n\nEnable logger to see what name BFF is searching for vs. what the env var contains.\n\n**Bundler plugin throws `did not expose __bff_discovery`**\n\nBFF's entry-point execution mode requires `tsx` to be able to require TypeScript files. Either:\n1. Add `tsx` as a devDependency in your bundler package\n2. Fall back to manual discovery overrides:\n\n```typescript\nnew BffWebpackPlugin({\n  entryPoint: resolve(__dirname, 'src/index.ts'),\n  // Manual fallback:\n  functionDirectoryPath: './functions',\n  searchGlob: '**/*.func.js',\n})\n```\n\n---\n\n## Requirements\n\n- Node.js ≥ 20\n- Firebase Functions Gen 1 or Gen 2\n- CJS modules for `exportFunctions`; ESM supported via `exportFunctionsAsync`\n- `tsx` as a dev dependency if using bundler plugins with TypeScript entry points\n\n---\n\n## Contributing\n\nThis is a Turborepo monorepo with npm workspaces.\n\n```sh\ngit clone https://github.com/george43g/better-firebase-functions\nnpm install\nnpm run build   # build all packages\nnpm test        # run all tests\nnpm run lint    # type-check all packages\n```\n\nCore library tests: `packages/core/__tests__/`\n\nE2E benchmarks against a real Firebase project:\n\n```sh\nPROJECT_ID=your-project-id ./e2e/run-deploy-benchmark.sh\n```\n\n---\n\n## License\n\n[MPL-2.0](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeorge43g%2Fbetter-firebase-functions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgeorge43g%2Fbetter-firebase-functions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeorge43g%2Fbetter-firebase-functions/lists"}