firebase-tools 15.5.0 → 15.6.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 (61) hide show
  1. package/lib/apiv2.js +11 -5
  2. package/lib/apphosting/backend.js +1 -1
  3. package/lib/commands/deploy.js +3 -0
  4. package/lib/commands/init.js +5 -0
  5. package/lib/config.js +1 -0
  6. package/lib/dataconnect/client.js +2 -2
  7. package/lib/dataconnect/prompts.js +14 -0
  8. package/lib/deploy/auth/deploy.js +63 -0
  9. package/lib/deploy/auth/index.js +9 -0
  10. package/lib/deploy/auth/prepare.js +26 -0
  11. package/lib/deploy/auth/release.js +5 -0
  12. package/lib/deploy/dataconnect/release.js +11 -0
  13. package/lib/deploy/functions/backend.js +10 -0
  14. package/lib/deploy/functions/build.js +2 -2
  15. package/lib/deploy/functions/checkIam.js +3 -2
  16. package/lib/deploy/functions/deploy.js +4 -2
  17. package/lib/deploy/functions/prepare.js +5 -2
  18. package/lib/deploy/functions/prepareFunctionsUpload.js +7 -5
  19. package/lib/deploy/functions/release/fabricator.js +96 -15
  20. package/lib/deploy/functions/release/index.js +2 -1
  21. package/lib/deploy/functions/runtimes/dart.js +42 -0
  22. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +4 -1
  23. package/lib/deploy/functions/runtimes/index.js +9 -1
  24. package/lib/deploy/functions/runtimes/supported/types.js +6 -0
  25. package/lib/deploy/index.js +2 -0
  26. package/lib/emulator/auth/index.js +23 -17
  27. package/lib/emulator/auth/operations.js +9 -1
  28. package/lib/emulator/downloadableEmulatorInfo.json +31 -31
  29. package/lib/emulator/hubExport.js +15 -1
  30. package/lib/ensureApiEnabled.js +2 -2
  31. package/lib/gcp/cloudbilling.js +6 -3
  32. package/lib/gcp/iam.js +1 -1
  33. package/lib/gcp/runv2.js +75 -3
  34. package/lib/gcp/serviceusage.js +2 -2
  35. package/lib/gemini/fdcExperience.js +0 -11
  36. package/lib/init/features/auth.js +72 -0
  37. package/lib/init/features/dataconnect/resolver.js +17 -3
  38. package/lib/init/features/functions/index.js +22 -14
  39. package/lib/init/features/functions/javascript.js +18 -3
  40. package/lib/init/features/functions/typescript.js +20 -3
  41. package/lib/init/features/functions/utils.js +10 -0
  42. package/lib/init/features/genkit/index.js +3 -2
  43. package/lib/init/features/index.js +6 -2
  44. package/lib/init/index.js +11 -1
  45. package/lib/management/provisioning/provision.js +3 -0
  46. package/lib/management/provisioning/types.js +7 -0
  47. package/lib/mcp/resources/guides/init_auth.js +19 -2
  48. package/lib/mcp/tools/apptesting/tests.js +6 -4
  49. package/lib/mcp/tools/core/init.js +31 -0
  50. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  51. package/package.json +1 -1
  52. package/schema/firebase-config.json +41 -0
  53. package/templates/init/apptesting/smoke_test.yaml +1 -1
  54. package/templates/init/dataconnect/secondary_schema.gql +8 -0
  55. package/templates/init/functions/javascript/index-ongraphrequest.js +36 -0
  56. package/templates/init/functions/javascript/package-ongraphrequest.lint.json +28 -0
  57. package/templates/init/functions/javascript/package-ongraphrequest.nolint.json +25 -0
  58. package/templates/init/functions/typescript/index-ongraphrequest.ts +44 -0
  59. package/templates/init/functions/typescript/package-ongraphrequest.lint.json +33 -0
  60. package/templates/init/functions/typescript/package-ongraphrequest.nolint.json +27 -0
  61. package/lib/mcp/util.test.js +0 -468
package/lib/apiv2.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Client = exports.CLI_OAUTH_PROJECT_NUMBER = exports.STANDARD_HEADERS = void 0;
3
+ exports.Client = exports.CLI_OAUTH_PROJECT_NUMBER = exports.GOOG_USER_PROJECT_HEADER = exports.STANDARD_HEADERS = void 0;
4
4
  exports.setRefreshToken = setRefreshToken;
5
5
  exports.setAccessToken = setAccessToken;
6
6
  exports.getAccessToken = getAccessToken;
@@ -29,7 +29,7 @@ exports.STANDARD_HEADERS = {
29
29
  "X-Client-Version": clientVersion,
30
30
  };
31
31
  const GOOG_QUOTA_USER_HEADER = "x-goog-quota-user";
32
- const GOOG_USER_PROJECT_HEADER = "x-goog-user-project";
32
+ exports.GOOG_USER_PROJECT_HEADER = "x-goog-user-project";
33
33
  const GOOGLE_CLOUD_QUOTA_PROJECT = process.env.GOOGLE_CLOUD_QUOTA_PROJECT;
34
34
  exports.CLI_OAUTH_PROJECT_NUMBER = "563584335869";
35
35
  let accessToken = "";
@@ -163,7 +163,7 @@ class Client {
163
163
  if (!reqOptions.ignoreQuotaProject &&
164
164
  GOOGLE_CLOUD_QUOTA_PROJECT &&
165
165
  GOOGLE_CLOUD_QUOTA_PROJECT !== "") {
166
- reqOptions.headers.set(GOOG_USER_PROJECT_HEADER, GOOGLE_CLOUD_QUOTA_PROJECT);
166
+ reqOptions.headers.set(exports.GOOG_USER_PROJECT_HEADER, GOOGLE_CLOUD_QUOTA_PROJECT);
167
167
  }
168
168
  return reqOptions;
169
169
  }
@@ -352,8 +352,14 @@ class Client {
352
352
  const logURL = this.requestURL(options);
353
353
  logger_1.logger.debug(`>>> [apiv2][query] ${options.method} ${logURL} ${queryParamsLog}`);
354
354
  const headers = options.headers;
355
- if (headers && headers.has(GOOG_QUOTA_USER_HEADER)) {
356
- logger_1.logger.debug(`>>> [apiv2][(partial)header] ${options.method} ${logURL} x-goog-quota-user=${headers.get(GOOG_QUOTA_USER_HEADER) || ""}`);
355
+ if (headers && (headers.has(GOOG_QUOTA_USER_HEADER) || headers.has(exports.GOOG_USER_PROJECT_HEADER))) {
356
+ const userHeader = headers.has(GOOG_QUOTA_USER_HEADER)
357
+ ? `${GOOG_QUOTA_USER_HEADER}=${headers.get(GOOG_QUOTA_USER_HEADER)}`
358
+ : "";
359
+ const projectHeader = headers.has(exports.GOOG_USER_PROJECT_HEADER)
360
+ ? `${exports.GOOG_USER_PROJECT_HEADER}=${headers.get(exports.GOOG_USER_PROJECT_HEADER)}`
361
+ : "";
362
+ logger_1.logger.debug(`>>> [apiv2][(partial)header] ${options.method} ${logURL} ${userHeader} ${projectHeader}`);
357
363
  }
358
364
  if (options.body !== undefined) {
359
365
  let logBody = "[omitted]";
@@ -190,7 +190,7 @@ async function ensureAppHostingComputeServiceAccount(projectId, serviceAccount)
190
190
  const sa = serviceAccount || defaultComputeServiceAccountEmail(projectId);
191
191
  const name = `projects/${projectId}/serviceAccounts/${sa}`;
192
192
  try {
193
- await iam.testResourceIamPermissions((0, api_1.iamOrigin)(), "v1", name, ["iam.serviceAccounts.actAs"], `projects/${projectId}`);
193
+ await iam.testResourceIamPermissions((0, api_1.iamOrigin)(), "v1", name, ["iam.serviceAccounts.actAs"], `${projectId}`);
194
194
  }
195
195
  catch (err) {
196
196
  if (!(err instanceof error_1.FirebaseError)) {
@@ -26,6 +26,7 @@ exports.VALID_DEPLOY_TARGETS = [
26
26
  "extensions",
27
27
  "dataconnect",
28
28
  "apphosting",
29
+ "auth",
29
30
  ];
30
31
  exports.TARGET_PERMISSIONS = {
31
32
  database: ["firebasedatabase.instances.update"],
@@ -73,6 +74,8 @@ exports.TARGET_PERMISSIONS = {
73
74
  "firebasedataconnect.schemas.list",
74
75
  "firebasedataconnect.schemas.update",
75
76
  ],
77
+ apphosting: [],
78
+ auth: ["firebase.projects.update", "firebaseauth.configs.update"],
76
79
  };
77
80
  exports.command = new command_1.Command("deploy")
78
81
  .description("deploy code and assets to your Firebase project")
@@ -88,6 +88,11 @@ let choices = [
88
88
  checked: false,
89
89
  hidden: true,
90
90
  },
91
+ {
92
+ value: "auth",
93
+ name: "Authentication: Set up Firebase Authentication",
94
+ checked: false,
95
+ },
91
96
  ];
92
97
  if ((0, experiments_1.isEnabled)("fdcwebhooks")) {
93
98
  choices.push({
package/lib/config.js CHANGED
@@ -261,6 +261,7 @@ Config.MATERIALIZE_TARGETS = [
261
261
  "remoteconfig",
262
262
  "dataconnect",
263
263
  "apphosting",
264
+ "auth",
264
265
  ];
265
266
  function stringifyContent(content) {
266
267
  if (typeof content === "string") {
@@ -117,8 +117,8 @@ async function upsertSchema(schema, validateOnly = false, async = false) {
117
117
  masterTimeout: 60000,
118
118
  });
119
119
  }
120
- async function deleteSchema(serviceName) {
121
- const op = await dataconnectClient().delete(`${serviceName}/schemas/${types.MAIN_SCHEMA_ID}`);
120
+ async function deleteSchema(name) {
121
+ const op = await dataconnectClient().delete(name);
122
122
  await operationPoller.pollOperation({
123
123
  apiOrigin: (0, api_1.dataconnectOrigin)(),
124
124
  apiVersion: DATACONNECT_API_VERSION,
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.promptDeleteConnector = promptDeleteConnector;
4
+ exports.promptDeleteSchema = promptDeleteSchema;
4
5
  const prompt_1 = require("../prompt");
5
6
  const utils = require("../utils");
6
7
  const client_1 = require("./client");
@@ -17,3 +18,16 @@ async function promptDeleteConnector(options, connectorName) {
17
18
  utils.logLabeledSuccess("dataconnect", `Connector ${connectorName} deleted`);
18
19
  }
19
20
  }
21
+ async function promptDeleteSchema(options, schemaName) {
22
+ utils.logLabeledWarning("dataconnect", `Schema ${schemaName} exists but is not listed in dataconnect.yaml.`);
23
+ const confirmDeletion = await (0, prompt_1.confirm)({
24
+ default: false,
25
+ message: `Do you want to delete the schema ${schemaName}?`,
26
+ force: options.force,
27
+ nonInteractive: options.nonInteractive,
28
+ });
29
+ if (confirmDeletion) {
30
+ await (0, client_1.deleteSchema)(schemaName);
31
+ utils.logLabeledSuccess("dataconnect", `Schema ${schemaName} deleted`);
32
+ }
33
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deploy = deploy;
4
+ const projectUtils_1 = require("../../projectUtils");
5
+ const provision_1 = require("../../management/provisioning/provision");
6
+ const apps_1 = require("../../management/apps");
7
+ const types_1 = require("../../management/provisioning/types");
8
+ const logger_1 = require("../../logger");
9
+ const utils_1 = require("../../utils");
10
+ async function deploy(context, options) {
11
+ const projectId = (0, projectUtils_1.needProjectId)(options);
12
+ const config = options.config.src.auth;
13
+ if (!config) {
14
+ return;
15
+ }
16
+ const appId = context.auth?.appId;
17
+ if (!appId) {
18
+ return;
19
+ }
20
+ const authInput = {};
21
+ const providers = config.providers;
22
+ const logMsg = [];
23
+ if (providers) {
24
+ if (providers.anonymous === true) {
25
+ logMsg.push("anonymous");
26
+ authInput.anonymousAuthProviderMode = types_1.ProviderMode.PROVIDER_ENABLED;
27
+ }
28
+ if (providers.emailPassword === true) {
29
+ logMsg.push("email/password");
30
+ authInput.emailAuthProviderMode = types_1.ProviderMode.PROVIDER_ENABLED;
31
+ }
32
+ if (providers.googleSignIn) {
33
+ logMsg.push("Google sign-in");
34
+ authInput.googleSigninProviderMode = types_1.ProviderMode.PROVIDER_ENABLED;
35
+ authInput.googleSigninProviderConfig = {
36
+ publicDisplayName: providers.googleSignIn.oAuthBrandDisplayName,
37
+ customerSupportEmail: providers.googleSignIn.supportEmail,
38
+ oauthRedirectUris: providers.googleSignIn.authorizedRedirectUris,
39
+ };
40
+ }
41
+ }
42
+ if (Object.keys(authInput).length === 0) {
43
+ logger_1.logger.debug("[auth] No auth providers configured to enable.");
44
+ return;
45
+ }
46
+ logger_1.logger.info(`Enabling auth providers: ${logMsg.join(", ")}...`);
47
+ await (0, provision_1.provisionFirebaseApp)({
48
+ project: {
49
+ parent: {
50
+ type: "existing_project",
51
+ projectId: projectId,
52
+ },
53
+ },
54
+ app: {
55
+ platform: apps_1.AppPlatform.WEB,
56
+ appId: appId,
57
+ },
58
+ features: {
59
+ firebaseAuthInput: authInput,
60
+ },
61
+ });
62
+ (0, utils_1.logSuccess)(`Auth providers enabled: ${logMsg.join(", ")}`);
63
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.release = exports.deploy = exports.prepare = void 0;
4
+ var prepare_1 = require("./prepare");
5
+ Object.defineProperty(exports, "prepare", { enumerable: true, get: function () { return prepare_1.prepare; } });
6
+ var deploy_1 = require("./deploy");
7
+ Object.defineProperty(exports, "deploy", { enumerable: true, get: function () { return deploy_1.deploy; } });
8
+ var release_1 = require("./release");
9
+ Object.defineProperty(exports, "release", { enumerable: true, get: function () { return release_1.release; } });
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepare = prepare;
4
+ const apps_1 = require("../../management/apps");
5
+ const logger_1 = require("../../logger");
6
+ const projectUtils_1 = require("../../projectUtils");
7
+ async function prepare(context, options) {
8
+ const projectId = (0, projectUtils_1.needProjectId)(options);
9
+ const config = options.config.src.auth;
10
+ if (!config) {
11
+ return;
12
+ }
13
+ const apps = await (0, apps_1.listFirebaseApps)(projectId, apps_1.AppPlatform.WEB);
14
+ let app = apps.find((a) => a.displayName === "Default Web App");
15
+ if (!app && apps.length > 0) {
16
+ app = apps[0];
17
+ }
18
+ if (!app) {
19
+ logger_1.logger.info("No Firebase Web App found. Creating 'Default Web App' for Auth provisioning...");
20
+ app = await (0, apps_1.createWebApp)(projectId, {
21
+ displayName: "Default Web App",
22
+ });
23
+ }
24
+ context.auth = context.auth || {};
25
+ context.auth.appId = app.appId;
26
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.release = release;
4
+ async function release() {
5
+ }
@@ -88,6 +88,13 @@ async function default_1(context, options) {
88
88
  for (const c of connectorsToDelete) {
89
89
  await (0, prompts_1.promptDeleteConnector)(options, c.name);
90
90
  }
91
+ const allSchemas = await deployedSchemas(serviceInfos);
92
+ const schemasToDelete = filters
93
+ ? []
94
+ : allSchemas.filter((s) => !wantSecondarySchemas.some((w) => w.name === s.name) && !(0, types_1.isMainSchema)(s));
95
+ for (const s of schemasToDelete) {
96
+ await (0, prompts_1.promptDeleteSchema)(options, s.name);
97
+ }
91
98
  let consolePath = "/dataconnect";
92
99
  if (serviceInfos.length === 1) {
93
100
  const sn = (0, names_1.parseServiceName)(serviceInfos[0].serviceName);
@@ -106,3 +113,7 @@ async function deployedConnectors(serviceInfos) {
106
113
  }
107
114
  return connectors;
108
115
  }
116
+ async function deployedSchemas(serviceInfos) {
117
+ const schemasPerService = await Promise.all(serviceInfos.map((si) => (0, client_1.listSchemas)(si.serviceName)));
118
+ return schemasPerService.flat();
119
+ }
@@ -29,6 +29,7 @@ exports.findEndpoint = findEndpoint;
29
29
  exports.matchingBackend = matchingBackend;
30
30
  exports.regionalEndpoints = regionalEndpoints;
31
31
  exports.compareFunctions = compareFunctions;
32
+ exports.maybeDeterministicCloudRunUri = maybeDeterministicCloudRunUri;
32
33
  const gcf = require("../../gcp/cloudfunctions");
33
34
  const gcfV2 = require("../../gcp/cloudfunctionsv2");
34
35
  const run = require("../../gcp/runv2");
@@ -340,3 +341,12 @@ function compareFunctions(left, right) {
340
341
  }
341
342
  return 0;
342
343
  }
344
+ function maybeDeterministicCloudRunUri(httpsFunc, projectNumber) {
345
+ const serviceName = httpsFunc.id.toLowerCase().replaceAll("_", "-");
346
+ const dnsSegment = `${serviceName}-${projectNumber}`;
347
+ if (dnsSegment.length > 63) {
348
+ logger_1.logger.info(`Function name ${httpsFunc.id} is too long to have a deterministic Cloud Run URI. Printing the non-deterministic URI instead.`);
349
+ return httpsFunc.uri;
350
+ }
351
+ return `https://${serviceName}-${projectNumber}.${httpsFunc.region}.run.app`;
352
+ }
@@ -55,7 +55,7 @@ function isBlockingTriggered(triggered) {
55
55
  return {}.hasOwnProperty.call(triggered, "blockingTrigger");
56
56
  }
57
57
  const allMemoryOptions = [128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768];
58
- exports.AllFunctionsPlatforms = ["gcfv1", "gcfv2"];
58
+ exports.AllFunctionsPlatforms = ["gcfv1", "gcfv2", "run"];
59
59
  exports.AllVpcEgressSettings = ["PRIVATE_RANGES_ONLY", "ALL_TRAFFIC"];
60
60
  exports.AllIngressSettings = [
61
61
  "ALLOW_ALL",
@@ -207,7 +207,7 @@ function toBackend(build, paramValues) {
207
207
  runtime: bdEndpoint.runtime,
208
208
  ...trigger,
209
209
  };
210
- proto.copyIfPresent(bkEndpoint, bdEndpoint, "environmentVariables", "labels", "secretEnvironmentVariables");
210
+ proto.copyIfPresent(bkEndpoint, bdEndpoint, "environmentVariables", "labels", "secretEnvironmentVariables", "baseImageUri", "command", "args");
211
211
  r.resolveStrings(bkEndpoint, bdEndpoint, "serviceAccount");
212
212
  proto.convertIfPresent(bkEndpoint, bdEndpoint, "ingressSettings", (from) => {
213
213
  if (from !== null && !backend.AllIngressSettings.includes(from)) {
@@ -52,8 +52,9 @@ async function checkHttpIam(context, options, payload) {
52
52
  const filters = context.filters || (0, functionsDeployHelper_1.getEndpointFilters)(options, context.config);
53
53
  const wantBackends = Object.values(payload.functions).map(({ wantBackend }) => wantBackend);
54
54
  const httpEndpoints = [...(0, functional_1.flattenArray)(wantBackends.map((b) => backend.allEndpoints(b)))]
55
- .filter(backend.isHttpsTriggered || backend.isDataConnectGraphqlTriggered)
56
- .filter((f) => (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(f, filters));
55
+ .filter((f) => backend.isHttpsTriggered(f) || backend.isDataConnectGraphqlTriggered(f))
56
+ .filter((f) => (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(f, filters))
57
+ .filter((f) => f.platform !== "run");
57
58
  const existing = await backend.existingBackend(context);
58
59
  const newHttpsEndpoints = httpEndpoints.filter(backend.missingEndpoint(existing));
59
60
  if (newHttpsEndpoints.length === 0) {
@@ -18,6 +18,7 @@ const experiments = require("../../experiments");
18
18
  const backend_1 = require("./backend");
19
19
  const extensions_1 = require("../extensions");
20
20
  const getProjectNumber_1 = require("../../getProjectNumber");
21
+ const path = require("path");
21
22
  (0, tmp_1.setGracefulCleanup)();
22
23
  async function uploadSourceV1(projectId, source, wantBackend) {
23
24
  const v1Endpoints = backend.allEndpoints(wantBackend).filter((e) => e.platform === "gcfv1");
@@ -53,7 +54,8 @@ async function uploadSourceV2(projectId, projectNumber, source, wantBackend) {
53
54
  file: source.functionsSourceV2,
54
55
  stream: exports.createReadStream(source.functionsSourceV2),
55
56
  };
56
- if (!experiments.isEnabled("runfunctions")) {
57
+ if (!experiments.isEnabled("functionsrunapionly") &&
58
+ !v2Endpoints.some((e) => e.platform === "run")) {
57
59
  if (process.env.GOOGLE_CLOUD_QUOTA_PROJECT) {
58
60
  (0, utils_1.logLabeledWarning)("functions", "GOOGLE_CLOUD_QUOTA_PROJECT is not usable when uploading source for Cloud Functions.");
59
61
  }
@@ -80,7 +82,7 @@ async function uploadSourceV2(projectId, projectNumber, source, wantBackend) {
80
82
  },
81
83
  },
82
84
  });
83
- const objectPath = `${source.functionsSourceV2Hash}.zip`;
85
+ const objectPath = `${source.functionsSourceV2Hash}${path.extname(source.functionsSourceV2)}`;
84
86
  await gcs.upload(uploadOpts, `${bucketName}/${objectPath}`, undefined, true);
85
87
  return {
86
88
  bucket: bucketName,
@@ -157,7 +157,7 @@ async function prepare(context, options, payload) {
157
157
  if (backend.someEndpoint(wantBackend, () => true)) {
158
158
  (0, utils_1.logLabeledBullet)("functions", `preparing ${clc.bold(sourceDirName)} directory for uploading...`);
159
159
  }
160
- if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
160
+ if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2" || e.platform === "run")) {
161
161
  const schPathSet = new Set();
162
162
  for (const e of backend.allEndpoints(wantBackend)) {
163
163
  if (backend.isDataConnectGraphqlTriggered(e) &&
@@ -165,7 +165,10 @@ async function prepare(context, options, payload) {
165
165
  schPathSet.add(e.dataConnectGraphqlTrigger.schemaFilePath);
166
166
  }
167
167
  }
168
- const packagedSource = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(options.config.projectDir, sourceDir, localCfg, [...schPathSet]);
168
+ const exportType = backend.someEndpoint(wantBackend, (e) => e.platform === "run")
169
+ ? "tar.gz"
170
+ : "zip";
171
+ const packagedSource = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(options.config.projectDir, sourceDir, localCfg, [...schPathSet], undefined, { exportType });
169
172
  source.functionsSourceV2 = packagedSource?.pathToSource;
170
173
  source.functionsSourceV2Hash = packagedSource?.hash;
171
174
  }
@@ -44,13 +44,15 @@ async function pipeAsync(from, to) {
44
44
  to.on("error", reject);
45
45
  });
46
46
  }
47
- async function packageSource(projectDir, sourceDir, config, additionalSources, runtimeConfig) {
48
- const tmpFile = tmp.fileSync({ prefix: "firebase-functions-", postfix: ".zip" }).name;
47
+ async function packageSource(projectDir, sourceDir, config, additionalSources, runtimeConfig, options) {
48
+ const exportType = options?.exportType || "zip";
49
+ const postfix = `.${exportType}`;
50
+ const tmpFile = tmp.fileSync({ prefix: "firebase-functions-", postfix }).name;
49
51
  const fileStream = fs.createWriteStream(tmpFile, {
50
52
  flags: "w",
51
53
  encoding: "binary",
52
54
  });
53
- const archive = archiver("zip");
55
+ const archive = exportType === "tar.gz" ? archiver("tar", { gzip: true }) : archiver("zip");
54
56
  const hashes = [];
55
57
  const ignore = config.ignore || ["node_modules", ".git"];
56
58
  ignore.push("firebase-debug.log", "firebase-debug.*.log", CONFIG_DEST_FILE);
@@ -110,8 +112,8 @@ async function packageSource(projectDir, sourceDir, config, additionalSources, r
110
112
  const hash = hashes.join(".");
111
113
  return { pathToSource: tmpFile, hash };
112
114
  }
113
- async function prepareFunctionsUpload(projectDir, sourceDir, config, additionalSources, runtimeConfig) {
114
- return packageSource(projectDir, sourceDir, config, additionalSources, runtimeConfig);
115
+ async function prepareFunctionsUpload(projectDir, sourceDir, config, additionalSources, runtimeConfig, options) {
116
+ return packageSource(projectDir, sourceDir, config, additionalSources, runtimeConfig, options);
115
117
  }
116
118
  function convertToSortedKeyValueArray(config) {
117
119
  if (typeof config !== "object" || config === null)
@@ -22,6 +22,7 @@ const poller = require("../../../operation-poller");
22
22
  const pubsub = require("../../../gcp/pubsub");
23
23
  const reporter = require("./reporter");
24
24
  const run = require("../../../gcp/run");
25
+ const runV2 = require("../../../gcp/runv2");
25
26
  const scheduler = require("../../../gcp/cloudscheduler");
26
27
  const utils = require("../../../utils");
27
28
  const services = require("../services");
@@ -138,9 +139,7 @@ class Fabricator {
138
139
  await this.createV2Function(endpoint, scraperV2);
139
140
  }
140
141
  else if (endpoint.platform === "run") {
141
- throw new error_1.FirebaseError("Creating new Cloud Run functions is not supported yet.", {
142
- exit: 1,
143
- });
142
+ await this.createRunFunction(endpoint);
144
143
  }
145
144
  else {
146
145
  (0, functional_1.assertExhaustive)(endpoint.platform);
@@ -161,7 +160,7 @@ class Fabricator {
161
160
  await this.updateV2Function(update.endpoint, scraperV2);
162
161
  }
163
162
  else if (update.endpoint.platform === "run") {
164
- throw new error_1.FirebaseError("Updating Cloud Run functions is not supported yet.", { exit: 1 });
163
+ await this.updateRunFunction(update);
165
164
  }
166
165
  else {
167
166
  (0, functional_1.assertExhaustive)(update.endpoint.platform);
@@ -177,7 +176,7 @@ class Fabricator {
177
176
  return this.deleteV2Function(endpoint);
178
177
  }
179
178
  else if (endpoint.platform === "run") {
180
- throw new error_1.FirebaseError("Deleting Cloud Run functions is not supported yet.", { exit: 1 });
179
+ return this.deleteRunFunction(endpoint);
181
180
  }
182
181
  (0, functional_1.assertExhaustive)(endpoint.platform);
183
182
  }
@@ -507,6 +506,80 @@ class Fabricator {
507
506
  }, { retryCodes: [...executor_1.DEFAULT_RETRY_CODES, CLOUD_RUN_RESOURCE_EXHAUSTED_CODE] })
508
507
  .catch(rethrowAs(endpoint, "delete"));
509
508
  }
509
+ async createRunFunction(endpoint) {
510
+ const storageSource = this.sources[endpoint.codebase]?.storage;
511
+ if (!storageSource) {
512
+ logger_1.logger.debug("Precondition failed. Cannot create a Cloud Run function without storage");
513
+ throw new Error("Precondition failed");
514
+ }
515
+ const service = runV2.serviceFromEndpoint(endpoint, "scratch");
516
+ const container = service.template.containers[0];
517
+ container.sourceCode = {
518
+ cloudStorageSource: {
519
+ bucket: storageSource.bucket,
520
+ object: storageSource.object,
521
+ generation: storageSource.generation ? String(storageSource.generation) : undefined,
522
+ },
523
+ };
524
+ await this.executor
525
+ .run(async () => {
526
+ const op = await runV2.createService(endpoint.project, endpoint.region, endpoint.id, service);
527
+ endpoint.uri = op.uri;
528
+ endpoint.runServiceId = endpoint.id;
529
+ })
530
+ .catch(rethrowAs(endpoint, "create"));
531
+ await this.setInvoker(endpoint);
532
+ }
533
+ async updateRunFunction(update) {
534
+ const endpoint = update.endpoint;
535
+ const storageSource = this.sources[endpoint.codebase]?.storage;
536
+ if (!storageSource) {
537
+ logger_1.logger.debug("Precondition failed. Cannot update a Cloud Run function without storage");
538
+ throw new Error("Precondition failed");
539
+ }
540
+ const service = runV2.serviceFromEndpoint(endpoint, "scratch");
541
+ const container = service.template.containers[0];
542
+ container.sourceCode = {
543
+ cloudStorageSource: {
544
+ bucket: storageSource.bucket,
545
+ object: storageSource.object,
546
+ generation: storageSource.generation ? String(storageSource.generation) : undefined,
547
+ },
548
+ };
549
+ await this.executor
550
+ .run(async () => {
551
+ const op = await runV2.updateService(service);
552
+ endpoint.uri = op.uri;
553
+ endpoint.runServiceId = endpoint.id;
554
+ })
555
+ .catch(rethrowAs(endpoint, "update"));
556
+ await this.setInvoker(endpoint);
557
+ }
558
+ async deleteRunFunction(endpoint) {
559
+ await this.executor
560
+ .run(async () => {
561
+ try {
562
+ await runV2.deleteService(endpoint.project, endpoint.region, endpoint.id);
563
+ }
564
+ catch (err) {
565
+ if (err.status === 404) {
566
+ return;
567
+ }
568
+ throw err;
569
+ }
570
+ })
571
+ .catch(rethrowAs(endpoint, "delete"));
572
+ }
573
+ async setInvoker(endpoint) {
574
+ if (backend.isHttpsTriggered(endpoint)) {
575
+ const invoker = endpoint.httpsTrigger.invoker || ["public"];
576
+ if (!invoker.includes("private")) {
577
+ await this.executor
578
+ .run(() => run.setInvokerUpdate(endpoint.project, `projects/${endpoint.project}/locations/${endpoint.region}/services/${endpoint.runServiceId}`, invoker))
579
+ .catch(rethrowAs(endpoint, "set invoker"));
580
+ }
581
+ }
582
+ }
510
583
  async setRunTraits(serviceName, endpoint) {
511
584
  await this.functionExecutor
512
585
  .run(async () => {
@@ -530,11 +603,6 @@ class Fabricator {
530
603
  .catch(rethrowAs(endpoint, "set concurrency"));
531
604
  }
532
605
  async setTrigger(endpoint) {
533
- if (endpoint.platform === "run") {
534
- throw new error_1.FirebaseError("Setting triggers for Cloud Run functions is not supported yet.", {
535
- exit: 1,
536
- });
537
- }
538
606
  if (backend.isScheduleTriggered(endpoint)) {
539
607
  if (endpoint.platform === "gcfv1") {
540
608
  await this.upsertScheduleV1(endpoint);
@@ -544,21 +612,25 @@ class Fabricator {
544
612
  await this.upsertScheduleV2(endpoint);
545
613
  return;
546
614
  }
615
+ else if (endpoint.platform === "run") {
616
+ throw new error_1.FirebaseError("Schedule triggers for Cloud Run functions are not supported yet.");
617
+ }
547
618
  (0, functional_1.assertExhaustive)(endpoint.platform);
548
619
  }
549
620
  else if (backend.isTaskQueueTriggered(endpoint)) {
621
+ if (endpoint.platform === "run") {
622
+ throw new error_1.FirebaseError("Task Queue triggers for Cloud Run functions are not supported yet.");
623
+ }
550
624
  await this.upsertTaskQueue(endpoint);
551
625
  }
552
626
  else if (backend.isBlockingTriggered(endpoint)) {
627
+ if (endpoint.platform === "run") {
628
+ throw new error_1.FirebaseError("Blocking triggers for Cloud Run functions are not supported yet.");
629
+ }
553
630
  await this.registerBlockingTrigger(endpoint);
554
631
  }
555
632
  }
556
633
  async deleteTrigger(endpoint) {
557
- if (endpoint.platform === "run") {
558
- throw new error_1.FirebaseError("Deleting triggers for Cloud Run functions is not supported yet.", {
559
- exit: 1,
560
- });
561
- }
562
634
  if (backend.isScheduleTriggered(endpoint)) {
563
635
  if (endpoint.platform === "gcfv1") {
564
636
  await this.deleteScheduleV1(endpoint);
@@ -568,12 +640,21 @@ class Fabricator {
568
640
  await this.deleteScheduleV2(endpoint);
569
641
  return;
570
642
  }
643
+ else if (endpoint.platform === "run") {
644
+ throw new error_1.FirebaseError("Schedule triggers for Cloud Run functions are not supported yet.");
645
+ }
571
646
  (0, functional_1.assertExhaustive)(endpoint.platform);
572
647
  }
573
648
  else if (backend.isTaskQueueTriggered(endpoint)) {
649
+ if (endpoint.platform === "run") {
650
+ throw new error_1.FirebaseError("Task Queue triggers for Cloud Run functions are not supported yet.");
651
+ }
574
652
  await this.disableTaskQueue(endpoint);
575
653
  }
576
654
  else if (backend.isBlockingTriggered(endpoint)) {
655
+ if (endpoint.platform === "run") {
656
+ throw new error_1.FirebaseError("Blocking triggers for Cloud Run functions are not supported yet.");
657
+ }
577
658
  await this.unregisterBlockingTrigger(endpoint);
578
659
  }
579
660
  }
@@ -107,7 +107,8 @@ function printTriggerUrls(results, projectNumber) {
107
107
  continue;
108
108
  }
109
109
  if (backend.isDataConnectGraphqlTriggered(httpsFunc)) {
110
- logger_1.logger.info(clc.bold("Function URL"), `(${(0, functionsDeployHelper_1.getFunctionLabel)(httpsFunc)}):`, `https://${httpsFunc.id.toLowerCase()}-${projectNumber}.${httpsFunc.region}.run.app`);
110
+ const uri = backend.maybeDeterministicCloudRunUri(httpsFunc, projectNumber);
111
+ logger_1.logger.info(clc.bold("Function URL"), `(${(0, functionsDeployHelper_1.getFunctionLabel)(httpsFunc)}):`, uri);
111
112
  continue;
112
113
  }
113
114
  logger_1.logger.info(clc.bold("Function URL"), `(${(0, functionsDeployHelper_1.getFunctionLabel)(httpsFunc)}):`, httpsFunc.uri);
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tryCreateDelegate = tryCreateDelegate;
4
+ const fs = require("fs-extra");
5
+ const path = require("path");
6
+ const yaml = require("js-yaml");
7
+ const discovery = require("./discovery");
8
+ async function tryCreateDelegate(context) {
9
+ const yamlPath = path.join(context.sourceDir, "functions.yaml");
10
+ if (!(await fs.pathExists(yamlPath))) {
11
+ return undefined;
12
+ }
13
+ const runtime = context.runtime || "dart3";
14
+ return {
15
+ language: "dart",
16
+ runtime: runtime,
17
+ bin: "",
18
+ validate: async () => {
19
+ try {
20
+ const content = await fs.readFile(yamlPath, "utf8");
21
+ yaml.load(content);
22
+ }
23
+ catch (e) {
24
+ throw new Error(`Failed to parse functions.yaml: ${e.message}`);
25
+ }
26
+ },
27
+ build: async () => {
28
+ return Promise.resolve();
29
+ },
30
+ watch: async () => {
31
+ return Promise.resolve(async () => {
32
+ });
33
+ },
34
+ discoverBuild: async () => {
35
+ const build = await discovery.detectFromYaml(context.sourceDir, context.projectId, runtime);
36
+ if (!build) {
37
+ throw new Error("Could not find functions.yaml");
38
+ }
39
+ return build;
40
+ },
41
+ };
42
+ }
@@ -82,6 +82,9 @@ function assertBuildEndpoint(ep, id) {
82
82
  taskQueueTrigger: "object",
83
83
  blockingTrigger: "object",
84
84
  cpu: (cpu) => cpu === null || isCEL(cpu) || cpu === "gcf_gen1" || typeof cpu === "number",
85
+ baseImageUri: "string?",
86
+ command: "array?",
87
+ args: "array?",
85
88
  });
86
89
  if (ep.vpc) {
87
90
  (0, parsing_1.assertKeyTypes)(prefix + ".vpc", ep.vpc, {
@@ -294,7 +297,7 @@ function parseEndpointForBuild(id, ep, project, defaultRegion, runtime) {
294
297
  if ("serviceAccountEmail" in ep) {
295
298
  parsed.serviceAccount = ep.serviceAccountEmail;
296
299
  }
297
- (0, proto_1.copyIfPresent)(parsed, ep, "omit", "availableMemoryMb", "cpu", "maxInstances", "minInstances", "concurrency", "timeoutSeconds", "vpc", "labels", "ingressSettings", "environmentVariables", "serviceAccount");
300
+ (0, proto_1.copyIfPresent)(parsed, ep, "omit", "availableMemoryMb", "cpu", "maxInstances", "minInstances", "concurrency", "timeoutSeconds", "vpc", "labels", "ingressSettings", "environmentVariables", "serviceAccount", "baseImageUri", "command", "args");
298
301
  (0, proto_1.convertIfPresent)(parsed, ep, "secretEnvironmentVariables", (senvs) => {
299
302
  if (!senvs) {
300
303
  return null;