Skip to content

Commit accea7a

Browse files
authored
Fix broken pnpm support for cf3 (#5467)
Due to the way pnpm resolves packages with peer dependencies, location of the firebase functions sdk package isn't where the CLI expects it to be. We make the logic for finding the binary associated Firebase Functions SDK more robust by checking list of possible paths where it might exist. We also add integration test for pnpm in the functions discovery test. Fixes #5448
1 parent 5811aea commit accea7a

File tree

8 files changed

+84
-26
lines changed

8 files changed

+84
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
- Fixes issue where `init firestore` was unecessarilly checking for default resource location. (#5230 and #5452)
44
- Pass `trailingSlash` from Next.js config to `firebase.json` (#5445)
55
- Don't use Next.js internal redirects for the backend test (#5445)
6+
- Fix issue where pnpm support broke for function emulation and deployment. (#5467)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const functions = require("firebase-functions");
2+
const { onRequest } = require("firebase-functions/v2/https");
3+
4+
exports.hellov1 = functions.https.onRequest((request, response) => {
5+
response.send("Hello from Firebase!");
6+
});
7+
8+
exports.hellov2 = onRequest((request, response) => {
9+
response.send("Hello from Firebase!");
10+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "pnpm",
3+
"dependencies": {
4+
"firebase-functions": "^4.0.0"
5+
},
6+
"engines": {
7+
"node": "16"
8+
}
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
set -euxo pipefail # bash strict mode
3+
IFS=$'\n\t'
4+
5+
cd functions && pnpm install

scripts/functions-discover-tests/run.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ firebase experiments:enable internaltesting
1111
# Install yarn
1212
npm i -g yarn
1313

14+
# Install pnpm
15+
npm install -g pnpm --force # it's okay to reinstall pnpm
16+
1417
for dir in ./scripts/functions-discover-tests/fixtures/*; do
1518
(cd $dir && ./install.sh)
1619
done

scripts/functions-discover-tests/tests.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ describe("Function discovery test", function (this) {
7777
},
7878
],
7979
},
80+
{
81+
name: "pnpm",
82+
projectDir: "pnpm",
83+
expects: [
84+
{
85+
codebase: "default",
86+
endpoints: ["hellov1", "hellov2"],
87+
},
88+
],
89+
},
8090
];
8191

8292
for (const tc of testCases) {

src/deploy/functions/runtimes/node/index.ts

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import * as runtimes from "..";
1717
import * as validate from "./validate";
1818
import * as versioning from "./versioning";
1919
import * as parseTriggers from "./parseTriggers";
20+
import { fileExistsSync } from "../../../../fsutils";
2021

2122
const MIN_FUNCTIONS_SDK_VERSION = "3.20.0";
2223

@@ -169,33 +170,51 @@ export class Delegate {
169170
if (Object.keys(config || {}).length) {
170171
env.CLOUD_RUNTIME_CONFIG = JSON.stringify(config);
171172
}
172-
// At this point, we've already confirmed that we found supported firebase functions sdk.
173+
// Location of the binary included in the Firebase Functions SDK
174+
// differs depending on the developer's setup and choice of package manager.
175+
//
176+
// We'll try few routes in the following order:
177+
//
178+
// 1. $SOURCE_DIR/node_modules/.bin/firebase-functions
179+
// 2. node_modules closest to the resolved path ${require.resolve("firebase-functions")}
180+
//
181+
// (1) works for most package managers (npm, yarn[no-hoist],pnpm).
182+
// (2) handles cases where developer prefers monorepo setup or bundled function code.
183+
const sourceNodeModulesPath = path.join(this.sourceDir, "node_modules");
173184
const sdkPath = require.resolve("firebase-functions", { paths: [this.sourceDir] });
174-
// Find location of the closest node_modules/ directory where we found the sdk.
175-
const binPath = sdkPath.substring(0, sdkPath.lastIndexOf("node_modules") + 12);
176-
// And execute the binary included in the sdk.
177-
const childProcess = spawn(path.join(binPath, ".bin", "firebase-functions"), [this.sourceDir], {
178-
env,
179-
cwd: this.sourceDir,
180-
stdio: [/* stdin=*/ "ignore", /* stdout=*/ "pipe", /* stderr=*/ "inherit"],
181-
});
182-
childProcess.stdout?.on("data", (chunk) => {
183-
logger.debug(chunk.toString());
184-
});
185-
return Promise.resolve(async () => {
186-
const p = new Promise<void>((resolve, reject) => {
187-
childProcess.once("exit", resolve);
188-
childProcess.once("error", reject);
189-
});
190-
191-
await fetch(`http://localhost:${port}/__/quitquitquit`);
192-
setTimeout(() => {
193-
if (!childProcess.killed) {
194-
childProcess.kill("SIGKILL");
195-
}
196-
}, 10_000);
197-
return p;
198-
});
185+
const sdkNodeModulesPath = sdkPath.substring(0, sdkPath.lastIndexOf("node_modules") + 12);
186+
for (const nodeModulesPath of [sourceNodeModulesPath, sdkNodeModulesPath]) {
187+
const binPath = path.join(nodeModulesPath, ".bin", "firebase-functions");
188+
if (fileExistsSync(binPath)) {
189+
logger.debug(`Found firebase-functions binary at '${binPath}'`);
190+
const childProcess = spawn(binPath, [this.sourceDir], {
191+
env,
192+
cwd: this.sourceDir,
193+
stdio: [/* stdin=*/ "ignore", /* stdout=*/ "pipe", /* stderr=*/ "inherit"],
194+
});
195+
childProcess.stdout?.on("data", (chunk) => {
196+
logger.debug(chunk.toString());
197+
});
198+
return Promise.resolve(async () => {
199+
const p = new Promise<void>((resolve, reject) => {
200+
childProcess.once("exit", resolve);
201+
childProcess.once("error", reject);
202+
});
203+
204+
await fetch(`http://localhost:${port}/__/quitquitquit`);
205+
setTimeout(() => {
206+
if (!childProcess.killed) {
207+
childProcess.kill("SIGKILL");
208+
}
209+
}, 10_000);
210+
return p;
211+
});
212+
}
213+
}
214+
throw new FirebaseError(
215+
"Failed to find location of Firebase Functions SDK. " +
216+
"Please file a bug on Github (https://github.com/firebase/firebase-tools/)."
217+
);
199218
}
200219

201220
// eslint-disable-next-line require-await

0 commit comments

Comments
 (0)