firebase-tools 14.10.0 → 14.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/lib/api.js +3 -1
  2. package/lib/apphosting/backend.js +5 -2
  3. package/lib/apptesting/invokeTests.js +40 -0
  4. package/lib/apptesting/parseTestFiles.js +75 -0
  5. package/lib/apptesting/types.js +18 -0
  6. package/lib/commands/apptesting-execute.js +102 -0
  7. package/lib/commands/index.js +4 -0
  8. package/lib/commands/init.js +7 -0
  9. package/lib/deploy/apphosting/prepare.js +1 -9
  10. package/lib/deploy/functions/runtimes/discovery/index.js +53 -4
  11. package/lib/deploy/functions/runtimes/node/index.js +74 -44
  12. package/lib/emulator/dataconnect/pgliteServer.js +4 -4
  13. package/lib/emulator/downloadableEmulatorInfo.json +18 -18
  14. package/lib/emulator/env.js +2 -1
  15. package/lib/emulator/hub.js +1 -2
  16. package/lib/emulator/tasksEmulator.js +1 -1
  17. package/lib/emulator/ui.js +3 -1
  18. package/lib/experiments.js +4 -0
  19. package/lib/firestore/api.js +1 -11
  20. package/lib/init/features/aitools/claude.js +44 -0
  21. package/lib/init/features/aitools/cursor.js +62 -0
  22. package/lib/init/features/aitools/gemini.js +58 -0
  23. package/lib/init/features/aitools/index.js +28 -0
  24. package/lib/init/features/aitools/promptUpdater.js +109 -0
  25. package/lib/init/features/aitools/studio.js +17 -0
  26. package/lib/init/features/aitools/types.js +2 -0
  27. package/lib/init/features/aitools.js +83 -0
  28. package/lib/init/features/apptesting/index.js +29 -0
  29. package/lib/init/features/index.js +4 -1
  30. package/lib/init/index.js +5 -0
  31. package/lib/mcp/index.js +32 -1
  32. package/lib/utils.js +21 -1
  33. package/package.json +2 -1
  34. package/prompts/FIREBASE.md +122 -0
  35. package/prompts/FIREBASE_FUNCTIONS.md +221 -0
  36. package/schema/apptesting-yaml.json +64 -0
  37. package/templates/init/aitools/cursor-rules-header.txt +8 -0
  38. package/templates/init/aitools/gemini-extension.json +11 -0
  39. package/templates/init/apptesting/smoke_test.yaml +6 -0
package/lib/api.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.storageOrigin = exports.runtimeconfigOrigin = exports.rulesOrigin = exports.resourceManagerOrigin = exports.crashlyticsApiOrigin = exports.messagingApiOrigin = exports.remoteConfigApiOrigin = exports.rtdbMetadataOrigin = exports.rtdbManagementOrigin = exports.realtimeOrigin = exports.extensionsTOSOrigin = exports.extensionsPublisherOrigin = exports.extensionsOrigin = exports.iamOrigin = exports.identityOrigin = exports.hostingOrigin = exports.googleOrigin = exports.pubsubOrigin = exports.cloudTasksOrigin = exports.cloudschedulerOrigin = exports.cloudbuildOrigin = exports.functionsDefaultRegion = exports.runOrigin = exports.functionsV2Origin = exports.functionsOrigin = exports.firestoreOrigin = exports.firestoreOriginOrEmulator = exports.firedataOrigin = exports.firebaseExtensionsRegistryOrigin = exports.firebaseApiOrigin = exports.eventarcOrigin = exports.dynamicLinksKey = exports.dynamicLinksOrigin = exports.consoleOrigin = exports.authManagementOrigin = exports.authOrigin = exports.apphostingGitHubAppInstallationURL = exports.apphostingP4SADomain = exports.apphostingOrigin = exports.appDistributionOrigin = exports.artifactRegistryDomain = exports.developerConnectP4SADomain = exports.developerConnectOrigin = exports.containerRegistryDomain = exports.cloudMonitoringOrigin = exports.cloudloggingOrigin = exports.cloudbillingOrigin = exports.clientSecret = exports.clientId = exports.authProxyOrigin = void 0;
4
- exports.setScopes = exports.getScopes = exports.cloudAiCompanionOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
4
+ exports.setScopes = exports.getScopes = exports.appTestingOrigin = exports.cloudAiCompanionOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
5
5
  const constants_1 = require("./emulator/constants");
6
6
  const logger_1 = require("./logger");
7
7
  const scopes = require("./scopes");
@@ -144,6 +144,8 @@ const vertexAIOrigin = () => utils.envOverride("VERTEX_AI_URL", "https://aiplatf
144
144
  exports.vertexAIOrigin = vertexAIOrigin;
145
145
  const cloudAiCompanionOrigin = () => utils.envOverride("CLOUD_AI_COMPANION_URL", "https://cloudaicompanion.googleapis.com");
146
146
  exports.cloudAiCompanionOrigin = cloudAiCompanionOrigin;
147
+ const appTestingOrigin = () => utils.envOverride("FIREBASE_APP_TESTING_URL", "https://firebaseapptesting.googleapis.com");
148
+ exports.appTestingOrigin = appTestingOrigin;
147
149
  function getScopes() {
148
150
  return Array.from(commandScopes);
149
151
  }
@@ -162,9 +162,12 @@ async function ensureAppHostingComputeServiceAccount(projectId, serviceAccount)
162
162
  if (!(err instanceof error_1.FirebaseError)) {
163
163
  throw err;
164
164
  }
165
- else if (err.status === 403) {
165
+ if (err.status === 403) {
166
166
  throw new error_1.FirebaseError(`Failed to create backend due to missing delegation permissions for ${sa}. Make sure you have the iam.serviceAccounts.actAs permission.`, { original: err });
167
167
  }
168
+ else if (err.status !== 404) {
169
+ throw new error_1.FirebaseError("Unexpected error occurred while testing for IAM service account permissions", { original: err });
170
+ }
168
171
  }
169
172
  await provisionDefaultComputeServiceAccount(projectId);
170
173
  }
@@ -232,7 +235,7 @@ async function provisionDefaultComputeServiceAccount(projectId) {
232
235
  }
233
236
  catch (err) {
234
237
  if ((0, error_1.getErrStatus)(err) === 400) {
235
- (0, utils_1.logWarning)("Your App Hosting compute service account is still being provisioned in the background; you may continue with the init flow.");
238
+ (0, utils_1.logWarning)("Your App Hosting compute service account is still being provisioned in the background. If you encounter an error, please try again after a few moments.");
236
239
  }
237
240
  else {
238
241
  throw err;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pollInvocationStatus = exports.invokeTests = void 0;
4
+ const apiv2_1 = require("../apiv2");
5
+ const api_1 = require("../api");
6
+ const operationPoller = require("../operation-poller");
7
+ const error_1 = require("../error");
8
+ const apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.appTestingOrigin)(), apiVersion: "v1alpha" });
9
+ async function invokeTests(appId, startUri, testDefs) {
10
+ const appResource = `projects/${appId.split(":")[1]}/apps/${appId}`;
11
+ try {
12
+ const invocationResponse = await apiClient.post(`${appResource}/testInvocations:invokeTestCases`, buildInvokeTestCasesRequest(testDefs));
13
+ return invocationResponse.body;
14
+ }
15
+ catch (err) {
16
+ throw new error_1.FirebaseError("Test invocation failed", { original: (0, error_1.getError)(err) });
17
+ }
18
+ }
19
+ exports.invokeTests = invokeTests;
20
+ function buildInvokeTestCasesRequest(testCaseInvocations) {
21
+ return {
22
+ resource: {
23
+ testInvocation: {},
24
+ testCaseInvocations,
25
+ },
26
+ };
27
+ }
28
+ async function pollInvocationStatus(operationName, onPoll, backoff = 30 * 1000) {
29
+ return operationPoller.pollOperation({
30
+ pollerName: "App Testing Invocation Poller",
31
+ apiOrigin: (0, api_1.appTestingOrigin)(),
32
+ apiVersion: "v1alpha",
33
+ operationResourceName: operationName,
34
+ masterTimeout: 30 * 60 * 1000,
35
+ backoff,
36
+ maxBackoff: 15 * 1000,
37
+ onPoll,
38
+ });
39
+ }
40
+ exports.pollInvocationStatus = pollInvocationStatus;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTestFiles = void 0;
4
+ const fsutils_1 = require("../fsutils");
5
+ const path_1 = require("path");
6
+ const logger_1 = require("../logger");
7
+ const types_1 = require("./types");
8
+ const utils_1 = require("../utils");
9
+ const error_1 = require("../error");
10
+ function createFilter(pattern) {
11
+ const regex = pattern ? new RegExp(pattern) : undefined;
12
+ return (s) => !regex || regex.test(s);
13
+ }
14
+ async function parseTestFiles(dir, targetUri, filePattern, namePattern) {
15
+ try {
16
+ new URL(targetUri);
17
+ }
18
+ catch (ex) {
19
+ const errMsg = "Invalid URL" + (targetUri.startsWith("http") ? "" : " (must include protocol)");
20
+ throw new error_1.FirebaseError(errMsg, { original: (0, error_1.getError)(ex) });
21
+ }
22
+ const fileFilterFn = createFilter(filePattern);
23
+ const nameFilterFn = createFilter(namePattern);
24
+ async function parseTestFilesRecursive(testDir) {
25
+ const items = (0, fsutils_1.listFiles)(testDir);
26
+ const results = [];
27
+ for (const item of items) {
28
+ const path = (0, path_1.join)(testDir, item);
29
+ if ((0, fsutils_1.dirExistsSync)(path)) {
30
+ results.push(...(await parseTestFilesRecursive(path)));
31
+ }
32
+ else if (fileFilterFn(path) && (0, fsutils_1.fileExistsSync)(path)) {
33
+ try {
34
+ const file = await (0, utils_1.readFileFromDirectory)(testDir, item);
35
+ const parsedFile = (0, utils_1.wrappedSafeLoad)(file.source);
36
+ const tests = parsedFile.tests;
37
+ const defaultConfig = parsedFile.defaultConfig;
38
+ if (!tests || !tests.length) {
39
+ logger_1.logger.info(`No tests found in ${path}. Ignoring.`);
40
+ continue;
41
+ }
42
+ for (const rawTestDef of parsedFile.tests) {
43
+ if (!nameFilterFn(rawTestDef.testName))
44
+ continue;
45
+ const testDef = toTestDef(rawTestDef, targetUri, defaultConfig);
46
+ results.push(testDef);
47
+ }
48
+ }
49
+ catch (ex) {
50
+ const errMsg = (0, error_1.getErrMsg)(ex);
51
+ const errDetails = errMsg ? `Error details: \n${errMsg}` : "";
52
+ logger_1.logger.info(`Unable to parse test file ${path}. Ignoring.${errDetails}`);
53
+ continue;
54
+ }
55
+ }
56
+ }
57
+ return results;
58
+ }
59
+ return parseTestFilesRecursive(dir);
60
+ }
61
+ exports.parseTestFiles = parseTestFiles;
62
+ function toTestDef(testDef, targetUri, defaultConfig) {
63
+ var _a, _b, _c, _d, _e, _f, _g;
64
+ const steps = (_a = testDef.steps) !== null && _a !== void 0 ? _a : [];
65
+ const route = (_d = (_c = (_b = testDef.testConfig) === null || _b === void 0 ? void 0 : _b.route) !== null && _c !== void 0 ? _c : defaultConfig === null || defaultConfig === void 0 ? void 0 : defaultConfig.route) !== null && _d !== void 0 ? _d : "";
66
+ const browsers = (_g = (_f = (_e = testDef.testConfig) === null || _e === void 0 ? void 0 : _e.browsers) !== null && _f !== void 0 ? _f : defaultConfig === null || defaultConfig === void 0 ? void 0 : defaultConfig.browsers) !== null && _g !== void 0 ? _g : [types_1.Browser.CHROME];
67
+ return {
68
+ testCase: {
69
+ startUri: targetUri + route,
70
+ displayName: testDef.testName,
71
+ instructions: { steps },
72
+ },
73
+ testExecution: browsers.map((browser) => ({ config: { browser } })),
74
+ };
75
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CompletionReason = exports.Browser = void 0;
4
+ var Browser;
5
+ (function (Browser) {
6
+ Browser["BROWSER_UNSPECIFIED"] = "BROWSER_UNSPECIFIED";
7
+ Browser["CHROME"] = "CHROME";
8
+ })(Browser = exports.Browser || (exports.Browser = {}));
9
+ var CompletionReason;
10
+ (function (CompletionReason) {
11
+ CompletionReason[CompletionReason["COMPLETION_REASON_UNSPECIFIED"] = 0] = "COMPLETION_REASON_UNSPECIFIED";
12
+ CompletionReason[CompletionReason["MAX_STEPS_REACHED"] = 1] = "MAX_STEPS_REACHED";
13
+ CompletionReason[CompletionReason["GOAL_FAILED"] = 2] = "GOAL_FAILED";
14
+ CompletionReason[CompletionReason["NO_ACTIONS_REQUIRED"] = 3] = "NO_ACTIONS_REQUIRED";
15
+ CompletionReason[CompletionReason["GOAL_INCONCLUSIVE"] = 4] = "GOAL_INCONCLUSIVE";
16
+ CompletionReason[CompletionReason["TEST_CANCELLED"] = 5] = "TEST_CANCELLED";
17
+ CompletionReason[CompletionReason["GOAL_SUCCEEDED"] = 6] = "GOAL_SUCCEEDED";
18
+ })(CompletionReason = exports.CompletionReason || (exports.CompletionReason = {}));
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const requireAuth_1 = require("../requireAuth");
5
+ const command_1 = require("../command");
6
+ const requireConfig_1 = require("../requireConfig");
7
+ const logger_1 = require("../logger");
8
+ const clc = require("colorette");
9
+ const parseTestFiles_1 = require("../apptesting/parseTestFiles");
10
+ const ora = require("ora");
11
+ const invokeTests_1 = require("../apptesting/invokeTests");
12
+ const error_1 = require("../error");
13
+ const marked_1 = require("marked");
14
+ const projectUtils_1 = require("../projectUtils");
15
+ const utils_1 = require("../utils");
16
+ const apps_1 = require("../management/apps");
17
+ exports.command = new command_1.Command("apptesting:execute <target>")
18
+ .description("Run automated tests written in natural language driven by AI")
19
+ .option("--app <app_id>", "The app id of your Firebase web app. Optional if the project contains exactly one web app.")
20
+ .option("--test-file-pattern <pattern>", "Test file pattern. Only tests contained in files that match this pattern will be executed.")
21
+ .option("--test-name-pattern <pattern>", "Test name pattern. Only tests with names that match this pattern will be executed.")
22
+ .option("--tests-non-blocking", "Request test execution without waiting for them to complete.")
23
+ .before(requireAuth_1.requireAuth)
24
+ .before(requireConfig_1.requireConfig)
25
+ .action(async (target, options) => {
26
+ var _a, _b;
27
+ const projectId = (0, projectUtils_1.needProjectId)(options);
28
+ const appList = await (0, apps_1.listFirebaseApps)(projectId, apps_1.AppPlatform.WEB);
29
+ let app = appList.find((a) => a.appId === options.app);
30
+ if (!app && appList.length === 1) {
31
+ app = appList[0];
32
+ logger_1.logger.info(`No app specified, defaulting to ${app.appId}`);
33
+ }
34
+ else if (!app) {
35
+ throw new error_1.FirebaseError("Invalid app id");
36
+ }
37
+ const testDir = ((_a = options.config.src.apptesting) === null || _a === void 0 ? void 0 : _a.testDir) || "tests";
38
+ const tests = await (0, parseTestFiles_1.parseTestFiles)(testDir, target, options.testFilePattern, options.testNamePattern);
39
+ if (!tests.length) {
40
+ throw new error_1.FirebaseError("No tests found");
41
+ }
42
+ const invokeSpinner = ora("Requesting test execution");
43
+ invokeSpinner.start();
44
+ let invocationOperation;
45
+ try {
46
+ invocationOperation = await (0, invokeTests_1.invokeTests)(app.appId, target, tests);
47
+ invokeSpinner.text = "Test execution requested";
48
+ invokeSpinner.succeed();
49
+ }
50
+ catch (ex) {
51
+ invokeSpinner.fail("Failed to request test execution");
52
+ throw ex;
53
+ }
54
+ logger_1.logger.info(clc.bold(`\n${clc.white("===")} Running ${pluralizeTests(tests.length)}`));
55
+ const invocationId = (_b = invocationOperation.name) === null || _b === void 0 ? void 0 : _b.split("/").pop();
56
+ const appWebId = app.webId;
57
+ const url = (0, utils_1.consoleUrl)(projectId, `/apptesting/app/web:${appWebId}/invocations/${invocationId}`);
58
+ logger_1.logger.info(await (0, marked_1.marked)(`**Invocation ID:** ${invocationId}`));
59
+ logger_1.logger.info(await (0, marked_1.marked)(`View progress and results in the [Firebase Console](${url})`));
60
+ if (options.testsNonBlocking) {
61
+ logger_1.logger.info("Not waiting for results");
62
+ return;
63
+ }
64
+ if (!invocationOperation.metadata) {
65
+ throw new error_1.FirebaseError("Invocation details unavailable");
66
+ }
67
+ const executionSpinner = ora(getOutput(invocationOperation.metadata));
68
+ executionSpinner.start();
69
+ const invocationOp = await (0, invokeTests_1.pollInvocationStatus)(invocationOperation.name, (operation) => {
70
+ if (!operation.done) {
71
+ executionSpinner.text = getOutput(operation.metadata);
72
+ }
73
+ });
74
+ const response = invocationOp.resource.testInvocation;
75
+ executionSpinner.text = `Testing complete\n${getOutput(response)}`;
76
+ if (response.failedExecutions || response.cancelledExecutions) {
77
+ executionSpinner.fail();
78
+ throw new error_1.FirebaseError("Testing complete with errors");
79
+ }
80
+ else {
81
+ executionSpinner.succeed();
82
+ }
83
+ });
84
+ function pluralizeTests(numTests) {
85
+ return `${numTests} test${numTests === 1 ? "" : "s"}`;
86
+ }
87
+ function getOutput(invocation) {
88
+ const output = [];
89
+ if (invocation.runningExecutions) {
90
+ output.push(`${pluralizeTests(invocation.runningExecutions)} running (this may take a while)...`);
91
+ }
92
+ if (invocation.succeededExecutions) {
93
+ output.push(`✔ ${pluralizeTests(invocation.succeededExecutions)} passed`);
94
+ }
95
+ if (invocation.failedExecutions) {
96
+ output.push(`✖ ${pluralizeTests(invocation.failedExecutions)} failed`);
97
+ }
98
+ if (invocation.cancelledExecutions) {
99
+ output.push(`⊝ ${pluralizeTests(invocation.cancelledExecutions)} cancelled`);
100
+ }
101
+ return output.length ? output.join("\n") : "Tests are starting";
102
+ }
@@ -228,6 +228,10 @@ function load(client) {
228
228
  client.target.clear = loadCommand("target-clear");
229
229
  client.target.remove = loadCommand("target-remove");
230
230
  client.use = loadCommand("use");
231
+ if (experiments.isEnabled("apptesting")) {
232
+ client.apptesting = {};
233
+ client.apptesting.execute = loadCommand("apptesting-execute");
234
+ }
231
235
  const t1 = process.hrtime.bigint();
232
236
  const diffMS = (t1 - t0) / BigInt(1e6);
233
237
  if (diffMS > 100) {
@@ -99,6 +99,13 @@ if ((0, experiments_1.isEnabled)("genkit")) {
99
99
  ...choices.slice(2),
100
100
  ];
101
101
  }
102
+ if ((0, experiments_1.isEnabled)("apptesting")) {
103
+ choices.push({
104
+ value: "apptesting",
105
+ name: "App Testing: create a smoke test",
106
+ checked: false,
107
+ });
108
+ }
102
109
  const featureNames = choices.map((choice) => choice.value);
103
110
  const HELP = `Interactively configure the current directory as a Firebase project or initialize new features in an already configured Firebase project directory.
104
111
 
@@ -13,15 +13,7 @@ async function default_1(context, options) {
13
13
  const projectId = (0, projectUtils_1.needProjectId)(options);
14
14
  await (0, apphosting_1.ensureApiEnabled)(options);
15
15
  await (0, backend_1.ensureRequiredApisEnabled)(projectId);
16
- try {
17
- await (0, backend_1.ensureAppHostingComputeServiceAccount)(projectId, "");
18
- }
19
- catch (err) {
20
- if (err.status === 400) {
21
- (0, utils_1.logLabeledWarning)("apphosting", "Your App Hosting compute service account is still being provisioned. Please try again in a few moments.");
22
- }
23
- throw err;
24
- }
16
+ await (0, backend_1.ensureAppHostingComputeServiceAccount)(projectId, "");
25
17
  context.backendConfigs = new Map();
26
18
  context.backendLocations = new Map();
27
19
  context.backendStorageUris = new Map();
@@ -1,16 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.detectFromPort = exports.detectFromYaml = exports.yamlToBuild = exports.getFunctionDiscoveryTimeout = exports.readFileAsync = void 0;
3
+ exports.detectFromOutputPath = exports.detectFromPort = exports.detectFromYaml = exports.yamlToBuild = exports.getFunctionDiscoveryTimeout = void 0;
4
4
  const node_fetch_1 = require("node-fetch");
5
5
  const fs = require("fs");
6
6
  const path = require("path");
7
7
  const yaml = require("yaml");
8
- const util_1 = require("util");
9
8
  const logger_1 = require("../../../../logger");
10
9
  const api = require("../../.../../../../api");
11
10
  const v1alpha1 = require("./v1alpha1");
12
11
  const error_1 = require("../../../../error");
13
- exports.readFileAsync = (0, util_1.promisify)(fs.readFile);
14
12
  const TIMEOUT_OVERRIDE_ENV_VAR = "FUNCTIONS_DISCOVERY_TIMEOUT";
15
13
  function getFunctionDiscoveryTimeout() {
16
14
  return +(process.env[TIMEOUT_OVERRIDE_ENV_VAR] || 0) * 1000;
@@ -34,7 +32,7 @@ exports.yamlToBuild = yamlToBuild;
34
32
  async function detectFromYaml(directory, project, runtime) {
35
33
  let text;
36
34
  try {
37
- text = await exports.readFileAsync(path.join(directory, "functions.yaml"), "utf8");
35
+ text = await fs.promises.readFile(path.join(directory, "functions.yaml"), "utf8");
38
36
  }
39
37
  catch (err) {
40
38
  if (err.code === "ENOENT") {
@@ -96,3 +94,54 @@ async function detectFromPort(port, project, runtime, initialDelay = 0, timeout
96
94
  return yamlToBuild(parsed, project, api.functionsDefaultRegion(), runtime);
97
95
  }
98
96
  exports.detectFromPort = detectFromPort;
97
+ async function detectFromOutputPath(childProcess, manifestPath, project, runtime, timeout = 10000) {
98
+ return new Promise((resolve, reject) => {
99
+ var _a;
100
+ let stderrBuffer = "";
101
+ let resolved = false;
102
+ const discoveryTimeout = getFunctionDiscoveryTimeout() || timeout;
103
+ const timer = setTimeout(() => {
104
+ if (!resolved) {
105
+ resolved = true;
106
+ reject(new error_1.FirebaseError(`User code failed to load. Cannot determine backend specification. Timeout after ${discoveryTimeout}ms`));
107
+ }
108
+ }, discoveryTimeout);
109
+ (_a = childProcess.stderr) === null || _a === void 0 ? void 0 : _a.on("data", (chunk) => {
110
+ stderrBuffer += chunk.toString();
111
+ });
112
+ childProcess.on("exit", async (code) => {
113
+ var _a;
114
+ if (!resolved) {
115
+ clearTimeout(timer);
116
+ resolved = true;
117
+ if (code !== 0 && code !== null) {
118
+ const errorMessage = (_a = stderrBuffer.trim()) !== null && _a !== void 0 ? _a : `Discovery process exited with code ${code}`;
119
+ reject(new error_1.FirebaseError(`User code failed to load. Cannot determine backend specification.\n${errorMessage}`));
120
+ }
121
+ else {
122
+ try {
123
+ const manifestContent = await fs.promises.readFile(manifestPath, "utf8");
124
+ const parsed = yaml.parse(manifestContent);
125
+ resolve(yamlToBuild(parsed, project, api.functionsDefaultRegion(), runtime));
126
+ }
127
+ catch (err) {
128
+ if (err.code === "ENOENT") {
129
+ reject(new error_1.FirebaseError(`Discovery process completed but no function manifest was found at ${manifestPath}`));
130
+ }
131
+ else {
132
+ reject(new error_1.FirebaseError(`Failed to read or parse manifest file: ${err.message}`));
133
+ }
134
+ }
135
+ }
136
+ }
137
+ });
138
+ childProcess.on("error", (err) => {
139
+ if (!resolved) {
140
+ clearTimeout(timer);
141
+ resolved = true;
142
+ reject(new error_1.FirebaseError(`Discovery process failed: ${err.message}`));
143
+ }
144
+ });
145
+ });
146
+ }
147
+ exports.detectFromOutputPath = detectFromOutputPath;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Delegate = exports.tryCreateDelegate = void 0;
4
- const util_1 = require("util");
4
+ const os = require("os");
5
5
  const fs = require("fs");
6
6
  const path = require("path");
7
7
  const portfinder = require("portfinder");
@@ -22,7 +22,10 @@ const MIN_FUNCTIONS_SDK_VERSION = "3.20.0";
22
22
  const MIN_FUNCTIONS_SDK_VERSION_FOR_EXTENSIONS_FEATURES = "5.1.0";
23
23
  async function tryCreateDelegate(context) {
24
24
  const packageJsonPath = path.join(context.sourceDir, "package.json");
25
- if (!(await (0, util_1.promisify)(fs.exists)(packageJsonPath))) {
25
+ try {
26
+ await fs.promises.access(packageJsonPath);
27
+ }
28
+ catch (_a) {
26
29
  logger_1.logger.debug("Customer code is not Node");
27
30
  return undefined;
28
31
  }
@@ -93,12 +96,7 @@ class Delegate {
93
96
  watch() {
94
97
  return Promise.resolve(() => Promise.resolve());
95
98
  }
96
- serveAdmin(port, config, envs) {
97
- var _a, _b;
98
- const env = Object.assign(Object.assign({}, envs), { PORT: port, FUNCTIONS_CONTROL_API: "true", HOME: process.env.HOME, PATH: process.env.PATH, NODE_ENV: process.env.NODE_ENV, __FIREBASE_FRAMEWORKS_ENTRY__: process.env.__FIREBASE_FRAMEWORKS_ENTRY__ });
99
- if (Object.keys(config || {}).length) {
100
- env.CLOUD_RUNTIME_CONFIG = JSON.stringify(config);
101
- }
99
+ findFunctionsBinary() {
102
100
  const sourceNodeModulesPath = path.join(this.sourceDir, "node_modules");
103
101
  const projectNodeModulesPath = path.join(this.projectDir, "node_modules");
104
102
  const sdkPath = require.resolve("firebase-functions", { paths: [this.sourceDir] });
@@ -113,40 +111,56 @@ class Delegate {
113
111
  const binPath = path.join(nodeModulesPath, ".bin", "firebase-functions");
114
112
  if ((0, fsutils_1.fileExistsSync)(binPath)) {
115
113
  logger_1.logger.debug(`Found firebase-functions binary at '${binPath}'`);
116
- const childProcess = spawn(binPath, [this.sourceDir], {
117
- env,
118
- cwd: this.sourceDir,
119
- stdio: ["ignore", "pipe", "pipe"],
120
- });
121
- (_a = childProcess.stdout) === null || _a === void 0 ? void 0 : _a.on("data", (chunk) => {
122
- logger_1.logger.info(chunk.toString("utf8"));
123
- });
124
- (_b = childProcess.stderr) === null || _b === void 0 ? void 0 : _b.on("data", (chunk) => {
125
- logger_1.logger.error(chunk.toString("utf8"));
126
- });
127
- return Promise.resolve(async () => {
128
- const p = new Promise((resolve, reject) => {
129
- childProcess.once("exit", resolve);
130
- childProcess.once("error", reject);
131
- });
132
- try {
133
- await (0, node_fetch_1.default)(`http://localhost:${port}/__/quitquitquit`);
134
- }
135
- catch (e) {
136
- logger_1.logger.debug("Failed to call quitquitquit. This often means the server failed to start", e);
137
- }
138
- setTimeout(() => {
139
- if (!childProcess.killed) {
140
- childProcess.kill("SIGKILL");
141
- }
142
- }, 10000);
143
- return p;
144
- });
114
+ return binPath;
145
115
  }
146
116
  }
147
117
  throw new error_1.FirebaseError("Failed to find location of Firebase Functions SDK. " +
148
118
  "Please file a bug on Github (https://github.com/firebase/firebase-tools/).");
149
119
  }
120
+ spawnFunctionsProcess(config, envs) {
121
+ var _a, _b;
122
+ const env = Object.assign(Object.assign({}, envs), { FUNCTIONS_CONTROL_API: "true", HOME: process.env.HOME, PATH: process.env.PATH, NODE_ENV: process.env.NODE_ENV, __FIREBASE_FRAMEWORKS_ENTRY__: process.env.__FIREBASE_FRAMEWORKS_ENTRY__ });
123
+ if (Object.keys(config || {}).length) {
124
+ env.CLOUD_RUNTIME_CONFIG = JSON.stringify(config);
125
+ }
126
+ const binPath = this.findFunctionsBinary();
127
+ const childProcess = spawn(binPath, [this.sourceDir], {
128
+ env,
129
+ cwd: this.sourceDir,
130
+ stdio: ["ignore", "pipe", "pipe"],
131
+ });
132
+ (_a = childProcess.stdout) === null || _a === void 0 ? void 0 : _a.on("data", (chunk) => {
133
+ logger_1.logger.info(chunk.toString("utf8"));
134
+ });
135
+ (_b = childProcess.stderr) === null || _b === void 0 ? void 0 : _b.on("data", (chunk) => {
136
+ logger_1.logger.error(chunk.toString("utf8"));
137
+ });
138
+ return childProcess;
139
+ }
140
+ execAdmin(config, envs, manifestPath) {
141
+ return this.spawnFunctionsProcess(config, Object.assign(Object.assign({}, envs), { FUNCTIONS_MANIFEST_OUTPUT_PATH: manifestPath }));
142
+ }
143
+ serveAdmin(config, envs, port) {
144
+ const childProcess = this.spawnFunctionsProcess(config, Object.assign(Object.assign({}, envs), { PORT: port }));
145
+ return Promise.resolve(async () => {
146
+ const p = new Promise((resolve, reject) => {
147
+ childProcess.once("exit", resolve);
148
+ childProcess.once("error", reject);
149
+ });
150
+ try {
151
+ await (0, node_fetch_1.default)(`http://localhost:${port}/__/quitquitquit`);
152
+ }
153
+ catch (e) {
154
+ logger_1.logger.debug("Failed to call quitquitquit. This often means the server failed to start", e);
155
+ }
156
+ setTimeout(() => {
157
+ if (!childProcess.killed) {
158
+ childProcess.kill("SIGKILL");
159
+ }
160
+ }, 10000);
161
+ return p;
162
+ });
163
+ }
150
164
  async discoverBuild(config, env) {
151
165
  if (!semver.valid(this.sdkVersion)) {
152
166
  logger_1.logger.debug(`Could not parse firebase-functions version '${this.sdkVersion}' into semver. Falling back to parseTriggers.`);
@@ -163,14 +177,30 @@ class Delegate {
163
177
  }
164
178
  let discovered = await discovery.detectFromYaml(this.sourceDir, this.projectId, this.runtime);
165
179
  if (!discovered) {
166
- const basePort = 8000 + (0, utils_1.randomInt)(0, 1000);
167
- const port = await portfinder.getPortPromise({ port: basePort });
168
- const kill = await this.serveAdmin(port.toString(), config, env);
169
- try {
170
- discovered = await discovery.detectFromPort(port, this.projectId, this.runtime);
180
+ const discoveryPath = process.env.FIREBASE_FUNCTIONS_DISCOVERY_OUTPUT_PATH;
181
+ if (!discoveryPath) {
182
+ const basePort = 8000 + (0, utils_1.randomInt)(0, 1000);
183
+ const port = await portfinder.getPortPromise({ port: basePort });
184
+ const kill = await this.serveAdmin(config, env, port.toString());
185
+ try {
186
+ discovered = await discovery.detectFromPort(port, this.projectId, this.runtime);
187
+ }
188
+ finally {
189
+ await kill();
190
+ }
191
+ }
192
+ else if (discoveryPath === "true") {
193
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "firebase-discovery-"));
194
+ const manifestPath = path.join(tempDir, "functions.yaml");
195
+ logger_1.logger.debug(`Writing functions discovery manifest to temporary file ${manifestPath}`);
196
+ const childProcess = this.execAdmin(config, env, manifestPath);
197
+ discovered = await discovery.detectFromOutputPath(childProcess, manifestPath, this.projectId, this.runtime);
171
198
  }
172
- finally {
173
- await kill();
199
+ else {
200
+ const manifestPath = path.join(discoveryPath, "functions.yaml");
201
+ logger_1.logger.debug(`Writing functions discovery manifest to ${manifestPath}`);
202
+ const childProcess = this.execAdmin(config, env, manifestPath);
203
+ discovered = await discovery.detectFromOutputPath(childProcess, manifestPath, this.projectId, this.runtime);
174
204
  }
175
205
  }
176
206
  return discovered;
@@ -44,7 +44,6 @@ BEGIN
44
44
  END
45
45
  $do$;`;
46
46
  const decoder = new node_string_decoder_1.StringDecoder();
47
- const pgliteDebugLog = fs.createWriteStream("pglite-debug.log");
48
47
  class PostgresServer {
49
48
  async createPGServer(host = "127.0.0.1", port) {
50
49
  const getDb = this.getDb.bind(this);
@@ -244,6 +243,7 @@ class PGliteExtendedQueryPatch {
244
243
  this.connection = connection;
245
244
  this.isExtendedQuery = false;
246
245
  this.eqpErrored = false;
246
+ this.pgliteDebugLog = fs.createWriteStream("pglite-debug.log");
247
247
  }
248
248
  filterResponse(message, response) {
249
249
  return __asyncGenerator(this, arguments, function* filterResponse_1() {
@@ -254,7 +254,7 @@ class PGliteExtendedQueryPatch {
254
254
  pg_gateway_1.FrontendMessageCode.Close,
255
255
  ];
256
256
  const decoded = decoder.write(message);
257
- pgliteDebugLog.write("Front: " + decoded);
257
+ this.pgliteDebugLog.write("Front: " + decoded);
258
258
  if (pipelineStartMessages.includes(message[0])) {
259
259
  this.isExtendedQuery = true;
260
260
  }
@@ -275,10 +275,10 @@ class PGliteExtendedQueryPatch {
275
275
  this.eqpErrored = true;
276
276
  }
277
277
  if (this.isExtendedQuery && bm[0] === pg_gateway_1.BackendMessageCode.ReadyForQuery) {
278
- pgliteDebugLog.write("Filtered: " + decoder.write(bm));
278
+ this.pgliteDebugLog.write("Filtered: " + decoder.write(bm));
279
279
  continue;
280
280
  }
281
- pgliteDebugLog.write("Sent: " + decoder.write(bm));
281
+ this.pgliteDebugLog.write("Sent: " + decoder.write(bm));
282
282
  yield yield __await(bm);
283
283
  }
284
284
  finally {