firebase-tools 14.10.1 → 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 (36) hide show
  1. package/lib/api.js +3 -1
  2. package/lib/apptesting/invokeTests.js +40 -0
  3. package/lib/apptesting/parseTestFiles.js +75 -0
  4. package/lib/apptesting/types.js +18 -0
  5. package/lib/commands/apptesting-execute.js +102 -0
  6. package/lib/commands/index.js +4 -0
  7. package/lib/commands/init.js +7 -0
  8. package/lib/deploy/functions/runtimes/discovery/index.js +53 -4
  9. package/lib/deploy/functions/runtimes/node/index.js +74 -44
  10. package/lib/emulator/downloadableEmulatorInfo.json +18 -18
  11. package/lib/emulator/env.js +2 -1
  12. package/lib/emulator/hub.js +1 -2
  13. package/lib/emulator/tasksEmulator.js +1 -1
  14. package/lib/emulator/ui.js +3 -1
  15. package/lib/experiments.js +4 -0
  16. package/lib/firestore/api.js +1 -11
  17. package/lib/init/features/aitools/claude.js +44 -0
  18. package/lib/init/features/aitools/cursor.js +62 -0
  19. package/lib/init/features/aitools/gemini.js +58 -0
  20. package/lib/init/features/aitools/index.js +28 -0
  21. package/lib/init/features/aitools/promptUpdater.js +109 -0
  22. package/lib/init/features/aitools/studio.js +17 -0
  23. package/lib/init/features/aitools/types.js +2 -0
  24. package/lib/init/features/aitools.js +83 -0
  25. package/lib/init/features/apptesting/index.js +29 -0
  26. package/lib/init/features/index.js +4 -1
  27. package/lib/init/index.js +5 -0
  28. package/lib/mcp/index.js +32 -1
  29. package/lib/utils.js +21 -1
  30. package/package.json +2 -1
  31. package/prompts/FIREBASE.md +122 -0
  32. package/prompts/FIREBASE_FUNCTIONS.md +221 -0
  33. package/schema/apptesting-yaml.json +64 -0
  34. package/templates/init/aitools/cursor-rules-header.txt +8 -0
  35. package/templates/init/aitools/gemini-extension.json +11 -0
  36. 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
  }
@@ -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
 
@@ -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;
@@ -54,28 +54,28 @@
54
54
  },
55
55
  "dataconnect": {
56
56
  "darwin": {
57
- "version": "2.9.1",
58
- "expectedSize": 29307744,
59
- "expectedChecksum": "b6c7485c7a25b703610172a5718e372c",
60
- "expectedChecksumSHA256": "2442af6068478fad6e202412ed41a9bb4ca663c27286e654042a346e8c7f1557",
61
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-v2.9.1",
62
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.9.1"
57
+ "version": "2.10.0",
58
+ "expectedSize": 29332320,
59
+ "expectedChecksum": "8e1f4303580e91017d449a091f987caa",
60
+ "expectedChecksumSHA256": "000356e1a93a8eb55ff6ba5e268b5dd4c0638636708e95874a74b859aa977cb3",
61
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-v2.10.0",
62
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.10.0"
63
63
  },
64
64
  "win32": {
65
- "version": "2.9.1",
66
- "expectedSize": 29797888,
67
- "expectedChecksum": "643ed6d0ef81488dc23f2ef5247d3f6f",
68
- "expectedChecksumSHA256": "ffc2723ce25ffedb958b848c71fcda5bf488208e9d2ef4310ac4f41d6533934d",
69
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-v2.9.1",
70
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.9.1.exe"
65
+ "version": "2.10.0",
66
+ "expectedSize": 29824000,
67
+ "expectedChecksum": "ea880276edee4908c4a6def6b1c7e2c1",
68
+ "expectedChecksumSHA256": "99ab61eba3de0cf14ef4ed5597f54e81ea8383b6782beba4b9b3f884acae4629",
69
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-v2.10.0",
70
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.10.0.exe"
71
71
  },
72
72
  "linux": {
73
- "version": "2.9.1",
74
- "expectedSize": 29233336,
75
- "expectedChecksum": "4ae414db7c87ebe8da9fe12139203d67",
76
- "expectedChecksumSHA256": "777e5ee6661cce1874f8c363d06ca446838b0e98840ec51b9aa8a79af6565db7",
77
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-v2.9.1",
78
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.9.1"
73
+ "version": "2.10.0",
74
+ "expectedSize": 29257912,
75
+ "expectedChecksum": "fa64e70507e70b8d8063ca0993a3b07a",
76
+ "expectedChecksumSHA256": "76880201ff7204c32cbbaa2c0cbf6a30528486eb1f2948064dcd218bceaab65e",
77
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-v2.10.0",
78
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.10.0"
79
79
  }
80
80
  }
81
81
  }
@@ -69,7 +69,8 @@ function maybeUsePortForwarding(i) {
69
69
  }
70
70
  const url = `${info.port}-${portForwardingHost}`;
71
71
  info.host = url;
72
- info.listen = (_a = info.listen) === null || _a === void 0 ? void 0 : _a.map((l) => {
72
+ info.listen = (_a = info.listen) === null || _a === void 0 ? void 0 : _a.map((listen) => {
73
+ const l = Object.assign({}, listen);
73
74
  l.address = url;
74
75
  l.port = 443;
75
76
  return l;
@@ -11,7 +11,6 @@ const hubExport_1 = require("./hubExport");
11
11
  const registry_1 = require("./registry");
12
12
  const ExpressBasedEmulator_1 = require("./ExpressBasedEmulator");
13
13
  const vsCodeUtils_1 = require("../vsCodeUtils");
14
- const env_1 = require("./env");
15
14
  const pkg = require("../../package.json");
16
15
  class EmulatorHub extends ExpressBasedEmulator_1.ExpressBasedEmulator {
17
16
  static readLocatorFile(projectId) {
@@ -45,7 +44,7 @@ class EmulatorHub extends ExpressBasedEmulator_1.ExpressBasedEmulator {
45
44
  getRunningEmulatorsMapping() {
46
45
  const emulators = {};
47
46
  for (const info of registry_1.EmulatorRegistry.listRunningWithInfo()) {
48
- emulators[info.name] = (0, env_1.maybeUsePortForwarding)(Object.assign({ listen: this.args.listenForEmulator[info.name] }, info));
47
+ emulators[info.name] = Object.assign({ listen: this.args.listenForEmulator[info.name] }, info);
49
48
  }
50
49
  return emulators;
51
50
  }
@@ -166,7 +166,7 @@ class TasksEmulator {
166
166
  }
167
167
  req.body.task.name =
168
168
  (_a = req.body.task.name) !== null && _a !== void 0 ? _a : `/projects/${projectId}/locations/${locationId}/queues/${queueName}/tasks/${Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)}`;
169
- req.body.task.httpRequest.body = JSON.parse(atob(req.body.task.httpRequest.body));
169
+ req.body.task.httpRequest.body = JSON.parse(Buffer.from(req.body.task.httpRequest.body, "base64").toString("utf-8"));
170
170
  const task = req.body.task;
171
171
  try {
172
172
  this.controller.enqueue(queueKey, task);
@@ -12,6 +12,8 @@ const constants_1 = require("./constants");
12
12
  const track_1 = require("../track");
13
13
  const ExpressBasedEmulator_1 = require("./ExpressBasedEmulator");
14
14
  const experiments_1 = require("../experiments");
15
+ const functional_1 = require("../functional");
16
+ const env_1 = require("./env");
15
17
  class EmulatorUI extends ExpressBasedEmulator_1.ExpressBasedEmulator {
16
18
  constructor(args) {
17
19
  super({
@@ -35,7 +37,7 @@ class EmulatorUI extends ExpressBasedEmulator_1.ExpressBasedEmulator {
35
37
  const downloadDetails = downloadableEmulators.getDownloadDetails(types_1.Emulators.UI);
36
38
  const webDir = path.join(downloadDetails.unzipDir, "client");
37
39
  app.get("/api/config", this.jsonHandler(() => {
38
- const emulatorInfos = hub.getRunningEmulatorsMapping();
40
+ const emulatorInfos = (0, functional_1.mapObject)(hub.getRunningEmulatorsMapping(), env_1.maybeUsePortForwarding);
39
41
  const json = Object.assign({ projectId, experiments: enabledExperiments !== null && enabledExperiments !== void 0 ? enabledExperiments : [], analytics: emulatorGaSession }, emulatorInfos);
40
42
  return Promise.resolve(json);
41
43
  }));
@@ -105,6 +105,10 @@ exports.ALL_EXPERIMENTS = experiments({
105
105
  default: true,
106
106
  public: false,
107
107
  },
108
+ apptesting: {
109
+ shortDescription: "Adds experimental App Testing feature",
110
+ public: true,
111
+ },
108
112
  });
109
113
  function isValidExperiment(name) {
110
114
  return Object.keys(exports.ALL_EXPERIMENTS).includes(name);