Firebase Functions Monorepo Deployments That Work
Monorepos bring huge advantages for code sharing and consistency, but deploying Firebase Functions from them often leads
to E404 Not Found
errors or TS6059
TypeScript issues. This occurs because Firebase's isolated build environment
doesn't understand local workspace packages or paths
aliases.
After navigating the challenges, a robust solution emerged: packing the shared package into a .tgz
tarball and using
an automated script (predeploy.js
) to manage its inclusion during the deployment process. This guide details this
specific, working workflow based on a real-world setup.
The Core Problem Recapโ
Firebase Functions deployment typically involves:
- Uploading only the specified
functions
directory. - Running
npm install
ornpm ci
based only on thepackage.json
(and potentiallypackage-lock.json
) found within that uploaded directory.
This breaks when npm
tries to find local workspace packages (like @your-org/common
) because they aren't on the
public registry and the rest of the workspace isn't present. TypeScript paths
pointing to local source can also cause
TS6059
(rootDir) errors during builds.
The Solution: Automated .tgz
Packingโ
This method ensures the shared package is treated like a regular dependency within the deployment package itself:
- Build & Pack: The shared
common
package is built (tsc
) and then packed into a.tgz
file (npm pack
). - Automate (
predeploy.js
): A script run before deployment handles:- Building and packing the
common
package. - Copying the generated
.tgz
into a specific folder within thefunctions
directory (e.g.,local_deps
). - Crucially: Modifying
functions/package.json
to change the@your-org/common
entry independencies
to point directly to this copied.tgz
file using thefile:
protocol (e.g.,file:local_deps/your-org-common-1.0.0.tgz
).
- Building and packing the
- Deploy:
firebase deploy
uploads thefunctions
directory, now containing the.tgz
file and the script-updatedpackage.json
. - Install: Firebase runs
npm install
, sees thefile:
path for@your-org/common
, and installs it directly from the included.tgz
, completely avoiding the public registry.
Step-by-Step Implementation (Based on Working Config)โ
Here are the key configuration files from a setup confirmed to work, assuming a typical monorepo structure:
my-monorepo/
โโโ packages/
โ โโโ common/ # @your-org/common package
โ โ โโโ src/
โ โ โโโ package.json
โ โ โโโ tsconfig.json
โ โโโ functions/ # Firebase Functions package
โ โโโ src/
โ โโโ predeploy.js # Automation Script
โ โโโ package.json
โ โโโ tsconfig.json # For IDE
โ โโโ tsconfig.build.json # For Build
โโโ package.json # Workspace root
โโโ firebase.json
1. Configure common
Package (packages/common
)
-
packages/common/package.json
: Define how the package is built and what's included when packed.{
"name": "@your-org/common",
"version": "1.0.0",
"private": true,
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"src"
],
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"devDependencies": {
"typescript": "^5.8.3"
}
} -
packages/common/tsconfig.json
: Configure the TypeScript build forcommon
. Ensuredeclaration
is true andcomposite
is false (or absent).{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"rootDir": "src",
"outDir": "lib",
"module": "commonjs",
"target": "es2018",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib"]
}
2. Create Automation Script (packages/functions/predeploy.js
)
This Node.js script handles the prepare-pack-copy-update process.
#!/usr/bin/env node
// packages/functions/predeploy.js
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
console.log('Starting transparent TGZ preparation...');
const functionsDir = path.resolve(__dirname);
const commonDir = path.resolve(functionsDir, '../common'); // Adjust if structure differs
const depsDir = path.join(functionsDir, 'local_deps');
const functionsPkgPath = path.join(functionsDir, 'package.json');
const lockfilePath = path.join(functionsDir, 'package-lock.json');
try {
if (fs.existsSync(lockfilePath)) {
console.log(`Deleting existing ${path.basename(lockfilePath)}...`);
fs.unlinkSync(lockfilePath);
console.log('Deleted lockfile.');
}
} catch (err) { console.warn(`WARN: Could not delete ${path.basename(lockfilePath)}`, err); }
try {
console.log(`Building common package in ${commonDir}...`);
execSync('npm run build', { cwd: commonDir, stdio: 'inherit' });
console.log('Common package built successfully.');
console.log(`Packing common package in ${commonDir}...`);
const tgzFilename = execSync('npm pack', { cwd: commonDir, encoding: 'utf-8' }).trim();
if (!tgzFilename || !tgzFilename.endsWith('.tgz')) { throw new Error(`'npm pack' did not output a valid .tgz filename. Output: "${tgzFilename}"`); }
const sourceTgzPath = path.join(commonDir, tgzFilename);
console.log(`Packed successfully: ${tgzFilename}`);
fs.mkdirSync(depsDir, { recursive: true });
const targetTgzPath = path.join(depsDir, tgzFilename);
console.log(`Copying ${tgzFilename} to ${depsDir}...`);
fs.copyFileSync(sourceTgzPath, targetTgzPath);
console.log('Copied successfully.');
console.log(`Updating ${path.basename(functionsPkgPath)}...`);
const pkgJson = JSON.parse(fs.readFileSync(functionsPkgPath, 'utf-8'));
if (!pkgJson.dependencies) { pkgJson.dependencies = {}; }
const relativeTgzPath = path.relative(functionsDir, targetTgzPath).replace(/\\/g, '/');
pkgJson.dependencies['@your-org/common'] = `file:${relativeTgzPath}`; // Use placeholder name
fs.writeFileSync(functionsPkgPath, JSON.stringify(pkgJson, null, 2) + '\n');
console.log(`Updated @your-org/common dependency to: file:${relativeTgzPath}`);
console.log('Transparent TGZ preparation complete.');
} catch (error) { console.error("\nโ Error during transparent TGZ preparation:", error); process.exit(1); }
3. Configure functions
TypeScript (Two-File Setup)
Use two files to separate IDE needs from build needs:
-
packages/functions/tsconfig.json
: (For IDE/Local Dev) - IncludesbaseUrl
andpaths
for local development convenience.// packages/functions/tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"esModuleInterop": true,
"outDir": "lib",
"sourceMap": true,
"strict": true,
"target": "es2018",
"lib": ["esnext", "dom"],
"skipLibCheck": true,
"moduleResolution": "node",
"declaration": true,
"rootDir": "src",
"baseUrl": ".",
"paths": {
"@your-org/common": ["../common/src"], // Use placeholder
"@your-org/common/*": ["../common/src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "lib", "local_deps"]
} -
packages/functions/tsconfig.build.json
: (For Build) - Does not extend. Manually lists required options, * *omittingbaseUrl
andpaths
**.// packages/functions/tsconfig.build.json
{
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"esModuleInterop": true,
"outDir": "lib",
"sourceMap": true, // Or false for production
"strict": true,
"target": "es2018",
"lib": ["esnext", "dom"],
"skipLibCheck": true,
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"declaration": false, // Usually false for final app build
"declarationMap": false,
"rootDir": "src"
},
"include": ["src"],
"exclude": [
"node_modules",
"lib",
"local_deps",
"**/*.spec.ts",
"**/*.test.ts"
]
}
4. Configure functions/package.json
Set up scripts and dependencies correctly. This reflects the working state where @your-org/common
is only listed in
dependencies
.
// packages/functions/package.json
{
"name": "your-functions-package", // Use placeholder name
"scripts": {
"lint": "eslint 'src/**/*'", // Adjust lint command as needed
"build": "tsc -p tsconfig.build.json", // Use the build config
"predeploy": "node predeploy.js", // Runs the automation script
// Deploy: runs predeploy, then build, then firebase deploy
// Use your preferred package manager runner (npm run, yarn, pnpm run)
"deploy": "npm run predeploy && npm run build && firebase deploy --only functions --project=YOUR_FIREBASE_PROJECT_ID",
"serve": "npm run build && firebase emulators:start --only functions" // Example serve
},
"engines": { "node": "20" },
"main": "lib/index.js", // Point to compiled output
"dependencies": {
// NOTE: predeploy.js updates this value dynamically.
// This shows the state *after* the script has run.
"@your-org/common": "file:local_deps/your-org-common-1.0.0.tgz", // Placeholder path/filename
// --- List true runtime dependencies from npm ---
"firebase-admin": "^12.2.0",
"firebase-functions": "^5.0.1"
// ... other external packages like axios, pg, etc.
},
"devDependencies": {
// NOTE: @your-org/common is OMITTED here per the provided working configuration.
// Local IDE features may rely on the tsconfig.json paths alias.
// --- List build tools, types etc. ---
"typescript": "^5.8.3",
"@types/node": "^20.0.0" // Match node engine
// ... other dev dependencies (eslint, types, jest, yarpm, etc.)
},
"private": true
}
5. Configure firebase.json
Ensure the predeploy
hook runs your automation script.
// my-monorepo/firebase.json
{
"functions": {
"source": "packages/functions", // Path to your functions package
"runtime": "nodejs20",
"predeploy": [
// Runs the 'predeploy' script from functions/package.json
// Ensure this script name matches ("predeploy")
"npm --prefix \"$RESOURCE_DIR\" run predeploy"
]
},
// ... other firebase config (hosting, etc.)
}
Deployment Workflow Summary:
- Run
npm run deploy
(or your package manager equivalent) inpackages/functions
. - The
predeploy
script inpackage.json
executesnode predeploy.js
via thefirebase.json
hook. predeploy.js
buildscommon
, packs it, copies the.tgz
, and updatespackage.json
dependencies
. It also deletes the lockfile found in the user's setup.- The
deploy
script inpackage.json
continues, runningnpm run build
(tsc -p tsconfig.build.json
), compilingfunctions
code using types resolved fromnode_modules
. - The
deploy
script finishes by runningfirebase deploy
. - Firebase CLI uploads the prepared
functions
directory. - Cloud Build runs
npm install
, installing@your-org/common
from the includedfile:local_deps/*.tgz
. - Deployment completes successfully.
This automated .tgz
workflow provides a robust solution for deploying Firebase Functions from monorepos with shared
local packages, keeping both deployment and local development working effectively based on the configurations provided.