From cb39406d214e143a076c8f5eedf2b6107f86b940 Mon Sep 17 00:00:00 2001 From: Abhiram Shaji Date: Thu, 29 Oct 2026 05:11:24 -0400 Subject: [PATCH 1/4] fix(functions): run the SDK bin entry when no .bin shim exists Deploy looked for an extensionless node_modules/.bin/firebase-functions file and gave up when it was missing. Bun on Windows only writes firebase-functions.exe and firebase-functions.bunx, so every deploy stopped with "Failed to find location of Firebase Functions SDK" even though the SDK itself resolved fine. The .bin lookup stays first so working setups are unchanged. When it finds nothing, the CLI now reads the bin entry the SDK declares in its own package.json and runs it with node, which is what the shim would have done. This does not depend on how a package manager names its shims or where it stores packages. Fixes #11386 --- CHANGELOG.md | 1 + .../functions/runtimes/node/index.spec.ts | 89 +++++++++++++++++++ src/deploy/functions/runtimes/node/index.ts | 37 +++++++- 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..040b06eb059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Fixed `firebase deploy` failing to find the Cloud Functions SDK when the package manager does not create a `node_modules/.bin/firebase-functions` file, such as Bun on Windows. (#11386) diff --git a/src/deploy/functions/runtimes/node/index.spec.ts b/src/deploy/functions/runtimes/node/index.spec.ts index 88066744961..1d9cff70b42 100644 --- a/src/deploy/functions/runtimes/node/index.spec.ts +++ b/src/deploy/functions/runtimes/node/index.spec.ts @@ -1,5 +1,7 @@ import { expect } from "chai"; import * as sinon from "sinon"; +import * as fs from "fs"; +import * as os from "os"; import * as path from "path"; import * as node from "."; @@ -136,4 +138,91 @@ describe("NodeDelegate", () => { expect(detectFromYamlMock).to.have.been.called; }); }); + describe("findFunctionsBinary", () => { + let projectDir: string; + let sourceDir: string; + + function installSdk(storeDir: string, binShims: string[], bin: unknown): string { + const nodeModules = path.join(sourceDir, "node_modules"); + const sdkDir = path.join(nodeModules, storeDir, "firebase-functions"); + fs.mkdirSync(path.join(sdkDir, "lib", "v2"), { recursive: true }); + fs.writeFileSync(path.join(sdkDir, "lib", "v2", "index.js"), ""); + fs.writeFileSync( + path.join(sdkDir, "package.json"), + JSON.stringify({ name: "firebase-functions", main: "lib/v2/index.js", bin }), + ); + if (storeDir) { + fs.symlinkSync(sdkDir, path.join(nodeModules, "firebase-functions"), "junction"); + } + fs.mkdirSync(path.join(nodeModules, ".bin"), { recursive: true }); + for (const shim of binShims) { + fs.writeFileSync(path.join(nodeModules, ".bin", shim), ""); + } + return fs.realpathSync(sdkDir); + } + + function findFunctionsBinary(): string[] { + const delegate = new node.Delegate(PROJECT_ID, projectDir, sourceDir, "nodejs24"); + return delegate["findFunctionsBinary"](); + } + + beforeEach(() => { + projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "functions-binary-")); + sourceDir = path.join(projectDir, "functions"); + fs.mkdirSync(sourceDir); + }); + + afterEach(() => { + fs.rmSync(projectDir, { recursive: true, force: true }); + }); + + it("prefers the node_modules/.bin shim when it exists", () => { + installSdk("", ["firebase-functions"], { + "firebase-functions": "lib/bin/firebase-functions.js", + }); + expect(findFunctionsBinary()).to.deep.equal([ + path.join(sourceDir, "node_modules", ".bin", "firebase-functions"), + ]); + }); + + it("runs the SDK bin entry with node when Bun on Windows only creates .exe shims", () => { + const sdkDir = installSdk( + path.join(".bun", "firebase-functions@7.4.0+abc", "node_modules"), + ["firebase-functions.exe", "firebase-functions.bunx"], + { "firebase-functions": "lib/bin/firebase-functions.js" }, + ); + expect(findFunctionsBinary()).to.deep.equal([ + "node", + path.join(sdkDir, "lib", "bin", "firebase-functions.js"), + ]); + }); + + it("runs the SDK bin entry with node when a .pnpm store only has .cmd shims", () => { + const sdkDir = installSdk( + path.join(".pnpm", "firebase-functions@7.4.0", "node_modules"), + ["firebase-functions.cmd", "firebase-functions.ps1"], + { "firebase-functions": "lib/bin/firebase-functions.js" }, + ); + expect(findFunctionsBinary()).to.deep.equal([ + "node", + path.join(sdkDir, "lib", "bin", "firebase-functions.js"), + ]); + }); + + it("accepts a bin entry declared as a single path", () => { + const sdkDir = installSdk("", [], "lib/bin/firebase-functions.js"); + expect(findFunctionsBinary()).to.deep.equal([ + "node", + path.join(sdkDir, "lib", "bin", "firebase-functions.js"), + ]); + }); + + it("throws when the SDK declares no bin entry and no shim exists", () => { + installSdk("", [], undefined); + expect(() => findFunctionsBinary()).to.throw( + FirebaseError, + "Failed to find location of Firebase Functions SDK", + ); + }); + }); }); diff --git a/src/deploy/functions/runtimes/node/index.ts b/src/deploy/functions/runtimes/node/index.ts index 4c56c4eeffc..abba6e4c13d 100644 --- a/src/deploy/functions/runtimes/node/index.ts +++ b/src/deploy/functions/runtimes/node/index.ts @@ -53,6 +53,29 @@ export async function tryCreateDelegate(context: DelegateContext): Promise; + }; + if (name !== "firebase-functions") { + continue; + } + const entry = typeof bin === "string" ? bin : bin?.["firebase-functions"]; + return entry ? path.join(dir, entry) : undefined; + } + return undefined; +} + // TODO(inlined): Consider moving contents in parseRuntimeAndValidateSDK and validate around. // Those two files are currently pretty coupled (e.g. they borrow error messages from each other) // and both files load package.json. Maybe the delegate should be constructed with a package.json and @@ -158,7 +181,7 @@ export class Delegate { return Promise.resolve(() => Promise.resolve()); } - private findFunctionsBinary(): string { + private findFunctionsBinary(): string[] { // Location of the binary included in the Firebase Functions SDK // differs depending on the developer's setup and choice of package manager. // @@ -188,10 +211,16 @@ export class Delegate { const binPath = path.join(nodeModulesPath, ".bin", "firebase-functions"); if (fileExistsSync(binPath)) { logger.debug(`Found firebase-functions binary at '${binPath}'`); - return binPath; + return [binPath]; } } + const binEntry = findSdkBinEntry(sdkPath); + if (binEntry) { + logger.debug(`Found firebase-functions bin entry at '${binEntry}'`); + return ["node", binEntry]; + } + throw new FirebaseError( "Failed to find location of Firebase Functions SDK. " + "Please file a bug on Github (https://github.com/firebase/firebase-tools/).", @@ -217,8 +246,8 @@ export class Delegate { env.CLOUD_RUNTIME_CONFIG = JSON.stringify(config); } - const binPath = this.findFunctionsBinary(); - const childProcess = spawn(binPath, [this.sourceDir], { + const [command, ...args] = this.findFunctionsBinary(); + const childProcess = spawn(command, [...args, this.sourceDir], { env, cwd: this.sourceDir, stdio: [/* stdin=*/ "ignore", /* stdout=*/ "pipe", /* stderr=*/ "pipe"], From 6e136d718dd88acc89b1f77905f020b3183db1ff Mon Sep 17 00:00:00 2001 From: Abhiram Shaji Date: Fri, 30 Oct 2026 12:15:10 -0400 Subject: [PATCH 2/4] fix(functions): run the SDK bin entry with the resolved node binary Use this.bin instead of the node on PATH, and ignore a bin entry that is not a string. --- .../functions/runtimes/node/index.spec.ts | 25 ++++++++----------- src/deploy/functions/runtimes/node/index.ts | 4 +-- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/deploy/functions/runtimes/node/index.spec.ts b/src/deploy/functions/runtimes/node/index.spec.ts index 1d9cff70b42..db3ae1317b9 100644 --- a/src/deploy/functions/runtimes/node/index.spec.ts +++ b/src/deploy/functions/runtimes/node/index.spec.ts @@ -161,9 +161,9 @@ describe("NodeDelegate", () => { return fs.realpathSync(sdkDir); } - function findFunctionsBinary(): string[] { + function findFunctionsBinary(): { binary: string[]; bin: string } { const delegate = new node.Delegate(PROJECT_ID, projectDir, sourceDir, "nodejs24"); - return delegate["findFunctionsBinary"](); + return { binary: delegate["findFunctionsBinary"](), bin: delegate.bin }; } beforeEach(() => { @@ -180,7 +180,8 @@ describe("NodeDelegate", () => { installSdk("", ["firebase-functions"], { "firebase-functions": "lib/bin/firebase-functions.js", }); - expect(findFunctionsBinary()).to.deep.equal([ + const { binary } = findFunctionsBinary(); + expect(binary).to.deep.equal([ path.join(sourceDir, "node_modules", ".bin", "firebase-functions"), ]); }); @@ -191,10 +192,8 @@ describe("NodeDelegate", () => { ["firebase-functions.exe", "firebase-functions.bunx"], { "firebase-functions": "lib/bin/firebase-functions.js" }, ); - expect(findFunctionsBinary()).to.deep.equal([ - "node", - path.join(sdkDir, "lib", "bin", "firebase-functions.js"), - ]); + const { binary, bin } = findFunctionsBinary(); + expect(binary).to.deep.equal([bin, path.join(sdkDir, "lib", "bin", "firebase-functions.js")]); }); it("runs the SDK bin entry with node when a .pnpm store only has .cmd shims", () => { @@ -203,18 +202,14 @@ describe("NodeDelegate", () => { ["firebase-functions.cmd", "firebase-functions.ps1"], { "firebase-functions": "lib/bin/firebase-functions.js" }, ); - expect(findFunctionsBinary()).to.deep.equal([ - "node", - path.join(sdkDir, "lib", "bin", "firebase-functions.js"), - ]); + const { binary, bin } = findFunctionsBinary(); + expect(binary).to.deep.equal([bin, path.join(sdkDir, "lib", "bin", "firebase-functions.js")]); }); it("accepts a bin entry declared as a single path", () => { const sdkDir = installSdk("", [], "lib/bin/firebase-functions.js"); - expect(findFunctionsBinary()).to.deep.equal([ - "node", - path.join(sdkDir, "lib", "bin", "firebase-functions.js"), - ]); + const { binary, bin } = findFunctionsBinary(); + expect(binary).to.deep.equal([bin, path.join(sdkDir, "lib", "bin", "firebase-functions.js")]); }); it("throws when the SDK declares no bin entry and no shim exists", () => { diff --git a/src/deploy/functions/runtimes/node/index.ts b/src/deploy/functions/runtimes/node/index.ts index abba6e4c13d..f2f4c479459 100644 --- a/src/deploy/functions/runtimes/node/index.ts +++ b/src/deploy/functions/runtimes/node/index.ts @@ -71,7 +71,7 @@ function findSdkBinEntry(sdkPath: string): string | undefined { continue; } const entry = typeof bin === "string" ? bin : bin?.["firebase-functions"]; - return entry ? path.join(dir, entry) : undefined; + return typeof entry === "string" && entry ? path.join(dir, entry) : undefined; } return undefined; } @@ -218,7 +218,7 @@ export class Delegate { const binEntry = findSdkBinEntry(sdkPath); if (binEntry) { logger.debug(`Found firebase-functions bin entry at '${binEntry}'`); - return ["node", binEntry]; + return [this.bin, binEntry]; } throw new FirebaseError( From 27cf57e7e217d28337de1a8605b3032e376da295 Mon Sep 17 00:00:00 2001 From: Abhiram Shaji Date: Tue, 3 Nov 2026 16:54:25 -0500 Subject: [PATCH 3/4] fix(functions): skip unreadable package.json files and require the bin entry to exist Export findSdkBinEntry and test it directly, covering a malformed package.json and a bin entry that points to a missing file. --- .../functions/runtimes/node/index.spec.ts | 74 ++++++++++++++----- src/deploy/functions/runtimes/node/index.ts | 28 ++++--- 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/src/deploy/functions/runtimes/node/index.spec.ts b/src/deploy/functions/runtimes/node/index.spec.ts index db3ae1317b9..3cd7139b110 100644 --- a/src/deploy/functions/runtimes/node/index.spec.ts +++ b/src/deploy/functions/runtimes/node/index.spec.ts @@ -139,6 +139,7 @@ describe("NodeDelegate", () => { }); }); describe("findFunctionsBinary", () => { + const BIN_ENTRY = path.join("lib", "bin", "firebase-functions.js"); let projectDir: string; let sourceDir: string; @@ -146,7 +147,9 @@ describe("NodeDelegate", () => { const nodeModules = path.join(sourceDir, "node_modules"); const sdkDir = path.join(nodeModules, storeDir, "firebase-functions"); fs.mkdirSync(path.join(sdkDir, "lib", "v2"), { recursive: true }); + fs.mkdirSync(path.join(sdkDir, "lib", "bin"), { recursive: true }); fs.writeFileSync(path.join(sdkDir, "lib", "v2", "index.js"), ""); + fs.writeFileSync(path.join(sdkDir, BIN_ENTRY), ""); fs.writeFileSync( path.join(sdkDir, "package.json"), JSON.stringify({ name: "firebase-functions", main: "lib/v2/index.js", bin }), @@ -161,6 +164,10 @@ describe("NodeDelegate", () => { return fs.realpathSync(sdkDir); } + function sdkMain(sdkDir: string): string { + return path.join(sdkDir, "lib", "v2", "index.js"); + } + function findFunctionsBinary(): { binary: string[]; bin: string } { const delegate = new node.Delegate(PROJECT_ID, projectDir, sourceDir, "nodejs24"); return { binary: delegate["findFunctionsBinary"](), bin: delegate.bin }; @@ -186,30 +193,14 @@ describe("NodeDelegate", () => { ]); }); - it("runs the SDK bin entry with node when Bun on Windows only creates .exe shims", () => { + it("runs the SDK bin entry with the resolved node binary when no shim exists", () => { const sdkDir = installSdk( path.join(".bun", "firebase-functions@7.4.0+abc", "node_modules"), ["firebase-functions.exe", "firebase-functions.bunx"], { "firebase-functions": "lib/bin/firebase-functions.js" }, ); const { binary, bin } = findFunctionsBinary(); - expect(binary).to.deep.equal([bin, path.join(sdkDir, "lib", "bin", "firebase-functions.js")]); - }); - - it("runs the SDK bin entry with node when a .pnpm store only has .cmd shims", () => { - const sdkDir = installSdk( - path.join(".pnpm", "firebase-functions@7.4.0", "node_modules"), - ["firebase-functions.cmd", "firebase-functions.ps1"], - { "firebase-functions": "lib/bin/firebase-functions.js" }, - ); - const { binary, bin } = findFunctionsBinary(); - expect(binary).to.deep.equal([bin, path.join(sdkDir, "lib", "bin", "firebase-functions.js")]); - }); - - it("accepts a bin entry declared as a single path", () => { - const sdkDir = installSdk("", [], "lib/bin/firebase-functions.js"); - const { binary, bin } = findFunctionsBinary(); - expect(binary).to.deep.equal([bin, path.join(sdkDir, "lib", "bin", "firebase-functions.js")]); + expect(binary).to.deep.equal([bin, path.join(sdkDir, BIN_ENTRY)]); }); it("throws when the SDK declares no bin entry and no shim exists", () => { @@ -219,5 +210,52 @@ describe("NodeDelegate", () => { "Failed to find location of Firebase Functions SDK", ); }); + + describe("findSdkBinEntry", () => { + it("finds the bin entry in a Bun store", () => { + const sdkDir = installSdk( + path.join(".bun", "firebase-functions@7.4.0+abc", "node_modules"), + ["firebase-functions.exe", "firebase-functions.bunx"], + { "firebase-functions": "lib/bin/firebase-functions.js" }, + ); + expect(node.findSdkBinEntry(sdkMain(sdkDir))).to.equal(path.join(sdkDir, BIN_ENTRY)); + }); + + it("finds the bin entry in a .pnpm store", () => { + const sdkDir = installSdk( + path.join(".pnpm", "firebase-functions@7.4.0", "node_modules"), + ["firebase-functions.cmd", "firebase-functions.ps1"], + { "firebase-functions": "lib/bin/firebase-functions.js" }, + ); + expect(node.findSdkBinEntry(sdkMain(sdkDir))).to.equal(path.join(sdkDir, BIN_ENTRY)); + }); + + it("accepts a bin entry declared as a single path", () => { + const sdkDir = installSdk("", [], "lib/bin/firebase-functions.js"); + expect(node.findSdkBinEntry(sdkMain(sdkDir))).to.equal(path.join(sdkDir, BIN_ENTRY)); + }); + + it("returns undefined when the SDK declares no bin entry", () => { + const sdkDir = installSdk("", [], undefined); + expect(node.findSdkBinEntry(sdkMain(sdkDir))).to.be.undefined; + }); + + it("returns undefined when the bin entry points to a missing file", () => { + const sdkDir = installSdk("", [], { "firebase-functions": "lib/bin/missing.js" }); + expect(node.findSdkBinEntry(sdkMain(sdkDir))).to.be.undefined; + }); + + it("skips a malformed package.json and keeps walking up", () => { + const sdkDir = installSdk("", [], "lib/bin/firebase-functions.js"); + fs.writeFileSync(path.join(sdkDir, "lib", "v2", "package.json"), "{"); + expect(node.findSdkBinEntry(sdkMain(sdkDir))).to.equal(path.join(sdkDir, BIN_ENTRY)); + }); + + it("returns undefined when the SDK package.json is malformed", () => { + const sdkDir = installSdk("", [], "lib/bin/firebase-functions.js"); + fs.writeFileSync(path.join(sdkDir, "package.json"), ""); + expect(node.findSdkBinEntry(sdkMain(sdkDir))).to.be.undefined; + }); + }); }); }); diff --git a/src/deploy/functions/runtimes/node/index.ts b/src/deploy/functions/runtimes/node/index.ts index f2f4c479459..6a8dc9f85f9 100644 --- a/src/deploy/functions/runtimes/node/index.ts +++ b/src/deploy/functions/runtimes/node/index.ts @@ -6,7 +6,7 @@ import * as semver from "semver"; import * as spawn from "cross-spawn"; import { ChildProcess } from "child_process"; -import { FirebaseError } from "../../../../error"; +import { FirebaseError, getErrMsg } from "../../../../error"; import { getRuntimeChoice } from "./parseRuntimeAndValidateSDK"; import { logger } from "../../../../logger"; import { @@ -53,7 +53,10 @@ export async function tryCreateDelegate(context: DelegateContext): Promise }; + +/** @internal */ +export function findSdkBinEntry(sdkPath: string): string | undefined { for ( let dir = path.dirname(sdkPath); path.basename(dir) !== "node_modules" && path.dirname(dir) !== dir; @@ -63,15 +66,22 @@ function findSdkBinEntry(sdkPath: string): string | undefined { if (!fileExistsSync(packageJsonPath)) { continue; } - const { name, bin } = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { - name?: string; - bin?: string | Record; - }; - if (name !== "firebase-functions") { + let pkg: SdkPackageJson | null; + try { + pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as SdkPackageJson | null; + } catch (err: unknown) { + logger.debug(`Failed to read ${packageJsonPath}: ${getErrMsg(err)}`); continue; } - const entry = typeof bin === "string" ? bin : bin?.["firebase-functions"]; - return typeof entry === "string" && entry ? path.join(dir, entry) : undefined; + if (pkg?.name !== "firebase-functions") { + continue; + } + const entry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["firebase-functions"]; + if (typeof entry !== "string" || !entry) { + return undefined; + } + const binEntry = path.join(dir, entry); + return fileExistsSync(binEntry) ? binEntry : undefined; } return undefined; } From 5c3587e6c05a553ea53dabfdbd47af363d317dc4 Mon Sep 17 00:00:00 2001 From: Abhiram Shaji Date: Tue, 3 Nov 2026 16:59:18 -0500 Subject: [PATCH 4/4] test(functions): check the SDK bin entry receives the source directory --- src/deploy/functions/runtimes/node/index.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/deploy/functions/runtimes/node/index.spec.ts b/src/deploy/functions/runtimes/node/index.spec.ts index 3cd7139b110..596b7e02eca 100644 --- a/src/deploy/functions/runtimes/node/index.spec.ts +++ b/src/deploy/functions/runtimes/node/index.spec.ts @@ -211,6 +211,20 @@ describe("NodeDelegate", () => { ); }); + it("passes the source directory to the SDK bin entry", async () => { + const sdkDir = installSdk("", [], "lib/bin/firebase-functions.js"); + fs.writeFileSync( + path.join(sdkDir, BIN_ENTRY), + "process.stdout.write(JSON.stringify(process.argv.slice(2)));", + ); + const delegate = new node.Delegate(PROJECT_ID, projectDir, sourceDir, "nodejs24"); + const child = delegate["spawnFunctionsProcess"]({}, {}); + let stdout = ""; + child.stdout?.on("data", (chunk: Buffer) => (stdout += chunk.toString("utf8"))); + await new Promise((resolve) => child.on("close", resolve)); + expect(stdout).to.equal(JSON.stringify([sourceDir])); + }); + describe("findSdkBinEntry", () => { it("finds the bin entry in a Bun store", () => { const sdkDir = installSdk(