firebase-tools 10.7.0 → 10.7.1

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.
@@ -40,7 +40,13 @@ exports.default = new command_1.Command("functions:delete [filters...]")
40
40
  if (options.region) {
41
41
  existingBackend.endpoints = { [options.region]: existingBackend.endpoints[options.region] };
42
42
  }
43
- const plan = planner.createDeploymentPlan(backend.empty(), existingBackend, context.filters, true);
43
+ const plan = planner.createDeploymentPlan({
44
+ wantBackend: backend.empty(),
45
+ haveBackend: existingBackend,
46
+ codebase: "",
47
+ filters: context.filters,
48
+ deleteAll: true,
49
+ });
44
50
  const allEpToDelete = Object.values(plan)
45
51
  .map((changes) => changes.endpointsToDelete)
46
52
  .reduce(functional_1.reduceFlat, [])
@@ -69,8 +75,9 @@ exports.default = new command_1.Command("functions:delete [filters...]")
69
75
  try {
70
76
  const fab = new fabricator.Fabricator({
71
77
  functionExecutor,
72
- executor: new executor.QueueExecutor({}),
73
78
  appEngineLocation,
79
+ executor: new executor.QueueExecutor({}),
80
+ sources: {},
74
81
  });
75
82
  const summary = await fab.applyPlan(plan);
76
83
  await reporter.logAndTrackDeployStats(summary);
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.compareFunctions = exports.missingEndpoint = exports.hasEndpoint = exports.regionalEndpoints = exports.matchingBackend = exports.findEndpoint = exports.someEndpoint = exports.allEndpoints = exports.checkAvailability = exports.existingBackend = exports.scheduleIdForFunction = exports.functionName = exports.isEmptyBackend = exports.of = exports.empty = exports.isBlockingTriggered = exports.isTaskQueueTriggered = exports.isScheduleTriggered = exports.isEventTriggered = exports.isCallableTriggered = exports.isHttpsTriggered = exports.AllFunctionsPlatforms = exports.secretVersionName = exports.SCHEDULED_FUNCTION_LABEL = exports.MIN_MEMORY_FOR_CONCURRENCY = exports.DEFAULT_MEMORY = exports.memoryOptionDisplayName = exports.AllMemoryOptions = exports.AllIngressSettings = exports.AllVpcEgressSettings = exports.endpointTriggerType = void 0;
3
+ exports.compareFunctions = exports.missingEndpoint = exports.hasEndpoint = exports.regionalEndpoints = exports.matchingBackend = exports.findEndpoint = exports.someEndpoint = exports.allEndpoints = exports.checkAvailability = exports.existingBackend = exports.scheduleIdForFunction = exports.functionName = exports.isEmptyBackend = exports.merge = exports.of = exports.empty = exports.isBlockingTriggered = exports.isTaskQueueTriggered = exports.isScheduleTriggered = exports.isEventTriggered = exports.isCallableTriggered = exports.isHttpsTriggered = exports.AllFunctionsPlatforms = exports.secretVersionName = exports.SCHEDULED_FUNCTION_LABEL = exports.MIN_MEMORY_FOR_CONCURRENCY = exports.DEFAULT_MEMORY = exports.memoryOptionDisplayName = exports.AllMemoryOptions = exports.AllIngressSettings = exports.AllVpcEgressSettings = exports.endpointTriggerType = void 0;
4
4
  const gcf = require("../../gcp/cloudfunctions");
5
5
  const gcfV2 = require("../../gcp/cloudfunctionsv2");
6
6
  const utils = require("../../utils");
7
7
  const error_1 = require("../../error");
8
8
  const previews_1 = require("../../previews");
9
+ const functional_1 = require("../../functional");
9
10
  function endpointTriggerType(endpoint) {
10
11
  if (isScheduleTriggered(endpoint)) {
11
12
  return "scheduled";
@@ -102,6 +103,25 @@ function of(...endpoints) {
102
103
  return bkend;
103
104
  }
104
105
  exports.of = of;
106
+ function merge(...backends) {
107
+ const merged = of(...(0, functional_1.flattenArray)(backends.map((b) => allEndpoints(b))));
108
+ const apiToReasons = {};
109
+ for (const b of backends) {
110
+ for (const { api, reason } of b.requiredAPIs) {
111
+ const reasons = apiToReasons[api] || new Set();
112
+ if (reason) {
113
+ reasons.add(reason);
114
+ }
115
+ apiToReasons[api] = reasons;
116
+ }
117
+ merged.environmentVariables = Object.assign(Object.assign({}, merged.environmentVariables), b.environmentVariables);
118
+ }
119
+ for (const [api, reasons] of Object.entries(apiToReasons)) {
120
+ merged.requiredAPIs.push({ api, reason: Array.from(reasons).join(" ") });
121
+ }
122
+ return merged;
123
+ }
124
+ exports.merge = merge;
105
125
  function isEmptyBackend(backend) {
106
126
  return (Object.keys(backend.requiredAPIs).length === 0 && Object.keys(backend.endpoints).length === 0);
107
127
  }
@@ -5,6 +5,7 @@ const cli_color_1 = require("cli-color");
5
5
  const logger_1 = require("../../logger");
6
6
  const functionsDeployHelper_1 = require("./functionsDeployHelper");
7
7
  const error_1 = require("../../error");
8
+ const functional_1 = require("../../functional");
8
9
  const iam = require("../../gcp/iam");
9
10
  const backend = require("./backend");
10
11
  const track_1 = require("../../track");
@@ -35,10 +36,12 @@ async function checkServiceAccountIam(projectId) {
35
36
  }
36
37
  exports.checkServiceAccountIam = checkServiceAccountIam;
37
38
  async function checkHttpIam(context, options, payload) {
39
+ if (!payload.functions) {
40
+ return;
41
+ }
38
42
  const filters = context.filters || (0, functionsDeployHelper_1.getEndpointFilters)(options);
39
- const wantBackend = payload.functions.wantBackend;
40
- const httpEndpoints = backend
41
- .allEndpoints(wantBackend)
43
+ const wantBackends = Object.values(payload.functions).map(({ wantBackend }) => wantBackend);
44
+ const httpEndpoints = [...(0, functional_1.flattenArray)(wantBackends.map((b) => backend.allEndpoints(b)))]
42
45
  .filter(backend.isHttpsTriggered)
43
46
  .filter((f) => (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(f, filters));
44
47
  const existing = await backend.existingBackend(context);
@@ -6,54 +6,62 @@ const clc = require("cli-color");
6
6
  const fs = require("fs");
7
7
  const checkIam_1 = require("./checkIam");
8
8
  const utils_1 = require("../../utils");
9
+ const projectConfig_1 = require("../../functions/projectConfig");
9
10
  const gcs = require("../../gcp/storage");
10
11
  const gcf = require("../../gcp/cloudfunctions");
11
12
  const gcfv2 = require("../../gcp/cloudfunctionsv2");
12
13
  const backend = require("./backend");
13
14
  (0, tmp_1.setGracefulCleanup)();
14
- async function uploadSourceV1(context, region) {
15
- const uploadUrl = await gcf.generateUploadUrl(context.projectId, region);
16
- context.source.sourceUrl = uploadUrl;
15
+ async function uploadSourceV1(projectId, source, wantBackend) {
16
+ const v1Endpoints = backend.allEndpoints(wantBackend).filter((e) => e.platform === "gcfv1");
17
+ if (v1Endpoints.length === 0) {
18
+ return;
19
+ }
20
+ const region = v1Endpoints[0].region;
21
+ const uploadUrl = await gcf.generateUploadUrl(projectId, region);
17
22
  const uploadOpts = {
18
- file: context.source.functionsSourceV1,
19
- stream: fs.createReadStream(context.source.functionsSourceV1),
23
+ file: source.functionsSourceV1,
24
+ stream: fs.createReadStream(source.functionsSourceV1),
20
25
  };
21
26
  await gcs.upload(uploadOpts, uploadUrl, {
22
27
  "x-goog-content-length-range": "0,104857600",
23
28
  });
29
+ return uploadUrl;
24
30
  }
25
- async function uploadSourceV2(context, region) {
26
- const res = await gcfv2.generateUploadUrl(context.projectId, region);
31
+ async function uploadSourceV2(projectId, source, wantBackend) {
32
+ const v2Endpoints = backend.allEndpoints(wantBackend).filter((e) => e.platform === "gcfv2");
33
+ if (v2Endpoints.length === 0) {
34
+ return;
35
+ }
36
+ const region = v2Endpoints[0].region;
37
+ const res = await gcfv2.generateUploadUrl(projectId, region);
27
38
  const uploadOpts = {
28
- file: context.source.functionsSourceV2,
29
- stream: fs.createReadStream(context.source.functionsSourceV2),
39
+ file: source.functionsSourceV2,
40
+ stream: fs.createReadStream(source.functionsSourceV2),
30
41
  };
31
42
  await gcs.upload(uploadOpts, res.uploadUrl);
32
- context.source.storage = res.storageSource;
43
+ return res.storageSource;
33
44
  }
34
- async function deploy(context, options, payload) {
35
- var _a, _b, _c, _d;
36
- if (!context.config) {
37
- return;
38
- }
39
- if (!((_a = context.source) === null || _a === void 0 ? void 0 : _a.functionsSourceV1) && !((_b = context.source) === null || _b === void 0 ? void 0 : _b.functionsSourceV2)) {
45
+ async function uploadCodebase(context, codebase, wantBackend) {
46
+ var _a;
47
+ const source = (_a = context.sources) === null || _a === void 0 ? void 0 : _a[codebase];
48
+ if (!source || (!source.functionsSourceV1 && !source.functionsSourceV2)) {
40
49
  return;
41
50
  }
42
- await (0, checkIam_1.checkHttpIam)(context, options, payload);
51
+ const uploads = [];
43
52
  try {
44
- const want = payload.functions.wantBackend;
45
- const uploads = [];
46
- const byPlatform = (0, utils_1.groupBy)(backend.allEndpoints(want), (e) => e.platform);
47
- if (((_c = byPlatform.gcfv1) === null || _c === void 0 ? void 0 : _c.length) > 0) {
48
- uploads.push(uploadSourceV1(context, byPlatform.gcfv1[0].region));
53
+ uploads.push(uploadSourceV1(context.projectId, source, wantBackend));
54
+ uploads.push(uploadSourceV2(context.projectId, source, wantBackend));
55
+ const [sourceUrl, storage] = await Promise.all(uploads);
56
+ if (sourceUrl) {
57
+ source.sourceUrl = sourceUrl;
49
58
  }
50
- if (((_d = byPlatform.gcfv2) === null || _d === void 0 ? void 0 : _d.length) > 0) {
51
- uploads.push(uploadSourceV2(context, byPlatform.gcfv2[0].region));
59
+ if (storage) {
60
+ source.storage = storage;
52
61
  }
53
- await Promise.all(uploads);
54
- const source = context.config.source;
62
+ const sourceDir = (0, projectConfig_1.configForCodebase)(context.config, codebase).source;
55
63
  if (uploads.length) {
56
- (0, utils_1.logSuccess)(`${clc.green.bold("functions:")} ${clc.bold(source)} folder uploaded successfully`);
64
+ (0, utils_1.logSuccess)(`${clc.green.bold("functions:")} ${clc.bold(sourceDir)} folder uploaded successfully`);
57
65
  }
58
66
  }
59
67
  catch (err) {
@@ -61,4 +69,18 @@ async function deploy(context, options, payload) {
61
69
  throw err;
62
70
  }
63
71
  }
72
+ async function deploy(context, options, payload) {
73
+ if (!context.config) {
74
+ return;
75
+ }
76
+ if (!payload.functions) {
77
+ return;
78
+ }
79
+ await (0, checkIam_1.checkHttpIam)(context, options, payload);
80
+ const uploads = [];
81
+ for (const [codebase, { wantBackend }] of Object.entries(payload.functions)) {
82
+ uploads.push(uploadCodebase(context, codebase, wantBackend));
83
+ }
84
+ await Promise.all(uploads);
85
+ }
64
86
  exports.deploy = deploy;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getFunctionLabel = exports.getEndpointFilters = exports.parseFunctionSelector = exports.endpointMatchesFilter = exports.endpointMatchesAnyFilter = void 0;
4
- const projectConfig = require("../../functions/projectConfig");
3
+ exports.groupEndpointsByCodebase = exports.targetCodebases = exports.getFunctionLabel = exports.getEndpointFilters = exports.parseFunctionSelector = exports.endpointMatchesFilter = exports.endpointMatchesAnyFilter = void 0;
4
+ const backend = require("./backend");
5
+ const projectConfig_1 = require("../../functions/projectConfig");
5
6
  function endpointMatchesAnyFilter(endpoint, filters) {
6
7
  if (!filters) {
7
8
  return true;
@@ -35,7 +36,7 @@ function parseFunctionSelector(selector) {
35
36
  if (fragments.length < 2) {
36
37
  return [
37
38
  { codebase: fragments[0] },
38
- { codebase: projectConfig.DEFAULT_CODEBASE, idChunks: fragments[0].split(/[-.]/) },
39
+ { codebase: projectConfig_1.DEFAULT_CODEBASE, idChunks: fragments[0].split(/[-.]/) },
39
40
  ];
40
41
  }
41
42
  return [
@@ -67,6 +68,49 @@ function getEndpointFilters(options) {
67
68
  }
68
69
  exports.getEndpointFilters = getEndpointFilters;
69
70
  function getFunctionLabel(fn) {
70
- return `${fn.id}(${fn.region})`;
71
+ let id = `${fn.id}(${fn.region})`;
72
+ if (fn.codebase && fn.codebase !== projectConfig_1.DEFAULT_CODEBASE) {
73
+ id = `[${fn.codebase}]${id}`;
74
+ }
75
+ return id;
71
76
  }
72
77
  exports.getFunctionLabel = getFunctionLabel;
78
+ function targetCodebases(config, filters) {
79
+ const codebasesFromConfig = [...new Set(Object.values(config).map((c) => c.codebase))];
80
+ if (!filters) {
81
+ return [...codebasesFromConfig];
82
+ }
83
+ const codebasesFromFilters = [
84
+ ...new Set(filters.map((f) => f.codebase).filter((c) => c !== undefined)),
85
+ ];
86
+ if (codebasesFromFilters.length === 0) {
87
+ return [...codebasesFromConfig];
88
+ }
89
+ const intersections = [];
90
+ for (const codebase of codebasesFromConfig) {
91
+ if (codebasesFromFilters.includes(codebase)) {
92
+ intersections.push(codebase);
93
+ }
94
+ }
95
+ return intersections;
96
+ }
97
+ exports.targetCodebases = targetCodebases;
98
+ function groupEndpointsByCodebase(wantBackends, haveEndpoints) {
99
+ const grouped = {};
100
+ let endpointsToAssign = haveEndpoints;
101
+ for (const codebase of Object.keys(wantBackends)) {
102
+ const names = backend.allEndpoints(wantBackends[codebase]).map((e) => backend.functionName(e));
103
+ grouped[codebase] = backend.of(...endpointsToAssign.filter((e) => names.includes(backend.functionName(e))));
104
+ endpointsToAssign = endpointsToAssign.filter((e) => !names.includes(backend.functionName(e)));
105
+ }
106
+ for (const codebase of Object.keys(wantBackends)) {
107
+ const matchedEndpoints = endpointsToAssign.filter((e) => e.codebase === codebase);
108
+ grouped[codebase] = backend.merge(grouped[codebase], backend.of(...matchedEndpoints));
109
+ const matchedNames = matchedEndpoints.map((e) => backend.functionName(e));
110
+ endpointsToAssign = endpointsToAssign.filter((e) => {
111
+ return !matchedNames.includes(backend.functionName(e));
112
+ });
113
+ }
114
+ return grouped;
115
+ }
116
+ exports.groupEndpointsByCodebase = groupEndpointsByCodebase;
@@ -25,16 +25,13 @@ const v1_1 = require("../../functions/events/v1");
25
25
  function hasUserConfig(config) {
26
26
  return Object.keys(config).length > 1;
27
27
  }
28
- function hasDotenv(opts) {
29
- return functionsEnv.hasUserEnvs(opts);
30
- }
31
28
  async function prepare(context, options, payload) {
32
29
  const projectId = (0, projectUtils_1.needProjectId)(options);
33
30
  const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
34
- context.config = (0, projectConfig_1.normalizeAndValidate)(options.config.src.functions)[0];
31
+ context.config = (0, projectConfig_1.normalizeAndValidate)(options.config.src.functions);
35
32
  context.filters = (0, functionsDeployHelper_1.getEndpointFilters)(options);
36
- if (context.filters &&
37
- !context.filters.map((f) => f.codebase).includes(context.config.codebase)) {
33
+ const codebases = (0, functionsDeployHelper_1.targetCodebases)(context.config, context.filters);
34
+ if (codebases.length === 0) {
38
35
  throw new error_1.FirebaseError("No function matches given --only filters. Aborting deployment.");
39
36
  }
40
37
  const checkAPIsEnabled = await Promise.all([
@@ -50,79 +47,97 @@ async function prepare(context, options, payload) {
50
47
  if (checkAPIsEnabled[1]) {
51
48
  runtimeConfig = Object.assign(Object.assign({}, runtimeConfig), (await (0, prepareFunctionsUpload_1.getFunctionsConfig)(projectId)));
52
49
  }
53
- (0, utils_1.logLabeledBullet)("functions", `preparing codebase ${clc.bold(context.config.codebase)} for deployment`);
54
- const sourceDirName = context.config.source;
55
- if (!sourceDirName) {
56
- throw new error_1.FirebaseError(`No functions code detected at default location (./functions), and no functions source defined in firebase.json`);
50
+ context.sources = {};
51
+ const codebaseUsesEnvs = [];
52
+ const wantBackends = {};
53
+ for (const codebase of codebases) {
54
+ (0, utils_1.logLabeledBullet)("functions", `preparing codebase ${clc.bold(codebase)} for deployment`);
55
+ const config = (0, projectConfig_1.configForCodebase)(context.config, codebase);
56
+ const sourceDirName = config.source;
57
+ if (!sourceDirName) {
58
+ throw new error_1.FirebaseError(`No functions code detected at default location (./functions), and no functions source defined in firebase.json`);
59
+ }
60
+ const sourceDir = options.config.path(sourceDirName);
61
+ const delegateContext = {
62
+ projectId,
63
+ sourceDir,
64
+ projectDir: options.config.projectDir,
65
+ runtime: config.runtime || "",
66
+ };
67
+ const runtimeDelegate = await runtimes.getRuntimeDelegate(delegateContext);
68
+ logger_1.logger.debug(`Validating ${runtimeDelegate.name} source`);
69
+ await runtimeDelegate.validate();
70
+ logger_1.logger.debug(`Building ${runtimeDelegate.name} source`);
71
+ await runtimeDelegate.build();
72
+ const firebaseEnvs = functionsEnv.loadFirebaseEnvs(firebaseConfig, projectId);
73
+ const userEnvOpt = {
74
+ functionsSource: sourceDir,
75
+ projectId: projectId,
76
+ projectAlias: options.projectAlias,
77
+ };
78
+ const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
79
+ logger_1.logger.debug(`Analyzing ${runtimeDelegate.name} backend spec`);
80
+ const wantBackend = await runtimeDelegate.discoverSpec(runtimeConfig, firebaseEnvs);
81
+ wantBackend.environmentVariables = Object.assign(Object.assign({}, userEnvs), firebaseEnvs);
82
+ for (const endpoint of backend.allEndpoints(wantBackend)) {
83
+ endpoint.environmentVariables = wantBackend.environmentVariables;
84
+ endpoint.codebase = codebase;
85
+ }
86
+ wantBackends[codebase] = wantBackend;
87
+ if (functionsEnv.hasUserEnvs(userEnvOpt)) {
88
+ codebaseUsesEnvs.push(codebase);
89
+ }
90
+ }
91
+ validate.endpointsAreUnique(wantBackends);
92
+ for (const [codebase, wantBackend] of Object.entries(wantBackends)) {
93
+ const config = (0, projectConfig_1.configForCodebase)(context.config, codebase);
94
+ const sourceDirName = config.source;
95
+ const sourceDir = options.config.path(sourceDirName);
96
+ const source = {};
97
+ if (backend.someEndpoint(wantBackend, () => true)) {
98
+ (0, utils_1.logLabeledBullet)("functions", `preparing ${clc.bold(sourceDirName)} directory for uploading...`);
99
+ }
100
+ if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
101
+ if (!previews_1.previews.functionsv2) {
102
+ throw new error_1.FirebaseError("This version of firebase-tools does not support Google Cloud " +
103
+ "Functions gen 2\n" +
104
+ "If Cloud Functions for Firebase gen 2 is still in alpha, sign up " +
105
+ "for the alpha program at " +
106
+ "https://services.google.com/fb/forms/firebasealphaprogram/\n" +
107
+ "If Cloud Functions for Firebase gen 2 is in beta, get the latest " +
108
+ "version of Firebse Tools with `npm i -g firebase-tools@latest`");
109
+ }
110
+ source.functionsSourceV2 = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, config);
111
+ }
112
+ if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv1")) {
113
+ source.functionsSourceV1 = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, config, runtimeConfig);
114
+ }
115
+ context.sources[codebase] = source;
116
+ }
117
+ payload.functions = {};
118
+ const haveBackends = (0, functionsDeployHelper_1.groupEndpointsByCodebase)(wantBackends, backend.allEndpoints(await backend.existingBackend(context)));
119
+ for (const [codebase, wantBackend] of Object.entries(wantBackends)) {
120
+ const haveBackend = haveBackends[codebase] || Object.assign({}, backend.empty());
121
+ payload.functions[codebase] = { wantBackend, haveBackend };
122
+ }
123
+ for (const [codebase, { wantBackend, haveBackend }] of Object.entries(payload.functions)) {
124
+ inferDetailsFromExisting(wantBackend, haveBackend, codebaseUsesEnvs.includes(codebase));
125
+ await (0, triggerRegionHelper_1.ensureTriggerRegions)(wantBackend);
126
+ validate.endpointsAreValid(wantBackend);
127
+ inferBlockingDetails(wantBackend);
57
128
  }
58
- const sourceDir = options.config.path(sourceDirName);
59
- const delegateContext = {
60
- projectId,
61
- sourceDir,
62
- projectDir: options.config.projectDir,
63
- runtime: context.config.runtime || "",
64
- };
65
- const runtimeDelegate = await runtimes.getRuntimeDelegate(delegateContext);
66
- logger_1.logger.debug(`Validating ${runtimeDelegate.name} source`);
67
- await runtimeDelegate.validate();
68
- logger_1.logger.debug(`Building ${runtimeDelegate.name} source`);
69
- await runtimeDelegate.build();
70
- const firebaseEnvs = functionsEnv.loadFirebaseEnvs(firebaseConfig, projectId);
71
- const userEnvOpt = {
72
- functionsSource: sourceDir,
73
- projectId: projectId,
74
- projectAlias: options.projectAlias,
75
- };
76
- const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
77
- const usedDotenv = hasDotenv(userEnvOpt);
78
129
  const tag = hasUserConfig(runtimeConfig)
79
- ? usedDotenv
130
+ ? codebaseUsesEnvs.length > 0
80
131
  ? "mixed"
81
132
  : "runtime_config"
82
- : usedDotenv
133
+ : codebaseUsesEnvs.length > 0
83
134
  ? "dotenv"
84
135
  : "none";
85
136
  void (0, track_1.track)("functions_codebase_deploy_env_method", tag);
86
- logger_1.logger.debug(`Analyzing ${runtimeDelegate.name} backend spec`);
87
- const wantBackend = await runtimeDelegate.discoverSpec(runtimeConfig, firebaseEnvs);
88
- wantBackend.environmentVariables = Object.assign(Object.assign({}, userEnvs), firebaseEnvs);
89
- for (const endpoint of backend.allEndpoints(wantBackend)) {
90
- endpoint.environmentVariables = wantBackend.environmentVariables;
91
- endpoint.codebase = context.config.codebase;
92
- }
93
- const source = {};
94
- if (backend.someEndpoint(wantBackend, () => true)) {
95
- (0, utils_1.logLabeledBullet)("functions", `preparing ${clc.bold(sourceDirName)} directory for uploading...`);
96
- }
97
- if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
98
- if (!previews_1.previews.functionsv2) {
99
- throw new error_1.FirebaseError("This version of firebase-tools does not support Google Cloud " +
100
- "Functions gen 2\n" +
101
- "If Cloud Functions for Firebase gen 2 is still in alpha, sign up " +
102
- "for the alpha program at " +
103
- "https://services.google.com/fb/forms/firebasealphaprogram/\n" +
104
- "If Cloud Functions for Firebase gen 2 is in beta, get the latest " +
105
- "version of Firebse Tools with `npm i -g firebase-tools@latest`");
106
- }
107
- source.functionsSourceV2 = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, context.config);
108
- }
109
- if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv1")) {
110
- source.functionsSourceV1 = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, context.config, runtimeConfig);
111
- }
112
- context.source = source;
113
- const wantEndpointNames = backend.allEndpoints(wantBackend).map((e) => backend.functionName(e));
114
- const haveBackend = backend.matchingBackend(await backend.existingBackend(context), (endpoint) => {
115
- var _a;
116
- if (endpoint.codebase === ((_a = context.config) === null || _a === void 0 ? void 0 : _a.codebase)) {
117
- return true;
118
- }
119
- return wantEndpointNames.includes(backend.functionName(endpoint));
120
- });
121
- inferDetailsFromExisting(wantBackend, haveBackend, usedDotenv);
122
- await (0, triggerRegionHelper_1.ensureTriggerRegions)(wantBackend);
123
- validate.endpointsAreValid(wantBackend);
124
- inferBlockingDetails(wantBackend);
125
- payload.functions = { wantBackend: wantBackend, haveBackend: haveBackend };
137
+ const codebaseCnt = Object.keys(payload.functions).length;
138
+ void (0, track_1.track)("functions_codebase_deploy_count", codebaseCnt >= 5 ? "5+" : codebaseCnt.toString());
139
+ const wantBackend = backend.merge(...Object.values(wantBackends));
140
+ const haveBackend = backend.merge(...Object.values(haveBackends));
126
141
  await Promise.all(Object.values(wantBackend.requiredAPIs).map(({ api }) => {
127
142
  return ensureApiEnabled.ensure(projectId, api, "functions", false);
128
143
  }));
@@ -44,8 +44,7 @@ class Fabricator {
44
44
  constructor(args) {
45
45
  this.executor = args.executor;
46
46
  this.functionExecutor = args.functionExecutor;
47
- this.sourceUrl = args.sourceUrl;
48
- this.storage = args.storage;
47
+ this.sources = args.sources;
49
48
  this.appEngineLocation = args.appEngineLocation;
50
49
  }
51
50
  async applyPlan(plan) {
@@ -154,12 +153,13 @@ class Fabricator {
154
153
  }
155
154
  }
156
155
  async createV1Function(endpoint, scraper) {
157
- var _a;
158
- if (!this.sourceUrl) {
156
+ var _a, _b;
157
+ const sourceUrl = (_a = this.sources[endpoint.codebase]) === null || _a === void 0 ? void 0 : _a.sourceUrl;
158
+ if (!sourceUrl) {
159
159
  logger_1.logger.debug("Precondition failed. Cannot create a GCF function without sourceUrl");
160
160
  throw new Error("Precondition failed");
161
161
  }
162
- const apiFunction = gcf.functionFromEndpoint(endpoint, this.sourceUrl);
162
+ const apiFunction = gcf.functionFromEndpoint(endpoint, sourceUrl);
163
163
  if (apiFunction.httpsTrigger) {
164
164
  apiFunction.httpsTrigger.securityLevel = "SECURE_ALWAYS";
165
165
  }
@@ -167,10 +167,10 @@ class Fabricator {
167
167
  const resultFunction = await this.functionExecutor
168
168
  .run(async () => {
169
169
  const op = await gcf.createFunction(apiFunction);
170
- return poller.pollOperation(Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `create-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, onPoll: scraper.poller }));
170
+ return poller.pollOperation(Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `create-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, onPoll: scraper.poller }));
171
171
  })
172
172
  .catch(rethrowAs(endpoint, "create"));
173
- endpoint.uri = (_a = resultFunction === null || resultFunction === void 0 ? void 0 : resultFunction.httpsTrigger) === null || _a === void 0 ? void 0 : _a.url;
173
+ endpoint.uri = (_b = resultFunction === null || resultFunction === void 0 ? void 0 : resultFunction.httpsTrigger) === null || _b === void 0 ? void 0 : _b.url;
174
174
  if (backend.isHttpsTriggered(endpoint)) {
175
175
  const invoker = endpoint.httpsTrigger.invoker || ["public"];
176
176
  if (!invoker.includes("private")) {
@@ -208,13 +208,14 @@ class Fabricator {
208
208
  }
209
209
  }
210
210
  async createV2Function(endpoint) {
211
- var _a;
212
- if (!this.storage) {
211
+ var _a, _b;
212
+ const storage = (_a = this.sources[endpoint.codebase]) === null || _a === void 0 ? void 0 : _a.storage;
213
+ if (!storage) {
213
214
  logger_1.logger.debug("Precondition failed. Cannot create a GCFv2 function without storage");
214
215
  throw new Error("Precondition failed");
215
216
  }
216
- const apiFunction = gcfV2.functionFromEndpoint(endpoint, this.storage);
217
- const topic = (_a = apiFunction.eventTrigger) === null || _a === void 0 ? void 0 : _a.pubsubTopic;
217
+ const apiFunction = gcfV2.functionFromEndpoint(endpoint, storage);
218
+ const topic = (_b = apiFunction.eventTrigger) === null || _b === void 0 ? void 0 : _b.pubsubTopic;
218
219
  if (topic) {
219
220
  await this.executor
220
221
  .run(async () => {
@@ -235,7 +236,7 @@ class Fabricator {
235
236
  const resultFunction = await this.functionExecutor
236
237
  .run(async () => {
237
238
  const op = await gcfV2.createFunction(apiFunction);
238
- return await poller.pollOperation(Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `create-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name }));
239
+ return await poller.pollOperation(Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `create-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name }));
239
240
  })
240
241
  .catch(rethrowAs(endpoint, "create"));
241
242
  endpoint.uri = resultFunction.serviceConfig.uri;
@@ -275,20 +276,21 @@ class Fabricator {
275
276
  }
276
277
  }
277
278
  async updateV1Function(endpoint, scraper) {
278
- var _a;
279
- if (!this.sourceUrl) {
279
+ var _a, _b;
280
+ const sourceUrl = (_a = this.sources[endpoint.codebase]) === null || _a === void 0 ? void 0 : _a.sourceUrl;
281
+ if (!sourceUrl) {
280
282
  logger_1.logger.debug("Precondition failed. Cannot update a GCF function without sourceUrl");
281
283
  throw new Error("Precondition failed");
282
284
  }
283
- const apiFunction = gcf.functionFromEndpoint(endpoint, this.sourceUrl);
285
+ const apiFunction = gcf.functionFromEndpoint(endpoint, sourceUrl);
284
286
  apiFunction.sourceToken = await scraper.tokenPromise();
285
287
  const resultFunction = await this.functionExecutor
286
288
  .run(async () => {
287
289
  const op = await gcf.updateFunction(apiFunction);
288
- return await poller.pollOperation(Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `update-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, onPoll: scraper.poller }));
290
+ return await poller.pollOperation(Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `update-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, onPoll: scraper.poller }));
289
291
  })
290
292
  .catch(rethrowAs(endpoint, "update"));
291
- endpoint.uri = (_a = resultFunction === null || resultFunction === void 0 ? void 0 : resultFunction.httpsTrigger) === null || _a === void 0 ? void 0 : _a.url;
293
+ endpoint.uri = (_b = resultFunction === null || resultFunction === void 0 ? void 0 : resultFunction.httpsTrigger) === null || _b === void 0 ? void 0 : _b.url;
292
294
  let invoker;
293
295
  if (backend.isHttpsTriggered(endpoint)) {
294
296
  invoker = endpoint.httpsTrigger.invoker;
@@ -307,19 +309,20 @@ class Fabricator {
307
309
  }
308
310
  }
309
311
  async updateV2Function(endpoint) {
310
- var _a;
311
- if (!this.storage) {
312
+ var _a, _b;
313
+ const storage = (_a = this.sources[endpoint.codebase]) === null || _a === void 0 ? void 0 : _a.storage;
314
+ if (!storage) {
312
315
  logger_1.logger.debug("Precondition failed. Cannot update a GCFv2 function without storage");
313
316
  throw new Error("Precondition failed");
314
317
  }
315
- const apiFunction = gcfV2.functionFromEndpoint(endpoint, this.storage);
316
- if ((_a = apiFunction.eventTrigger) === null || _a === void 0 ? void 0 : _a.pubsubTopic) {
318
+ const apiFunction = gcfV2.functionFromEndpoint(endpoint, storage);
319
+ if ((_b = apiFunction.eventTrigger) === null || _b === void 0 ? void 0 : _b.pubsubTopic) {
317
320
  delete apiFunction.eventTrigger.pubsubTopic;
318
321
  }
319
322
  const resultFunction = await this.functionExecutor
320
323
  .run(async () => {
321
324
  const op = await gcfV2.updateFunction(apiFunction);
322
- return await poller.pollOperation(Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `update-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name }));
325
+ return await poller.pollOperation(Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `update-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name }));
323
326
  })
324
327
  .catch(rethrowAs(endpoint, "update"));
325
328
  endpoint.uri = resultFunction.serviceConfig.uri;
@@ -349,7 +352,7 @@ class Fabricator {
349
352
  await this.functionExecutor
350
353
  .run(async () => {
351
354
  const op = await gcf.deleteFunction(fnName);
352
- const pollerOptions = Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `delete-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name });
355
+ const pollerOptions = Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `delete-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name });
353
356
  await poller.pollOperation(pollerOptions);
354
357
  })
355
358
  .catch(rethrowAs(endpoint, "delete"));
@@ -359,7 +362,7 @@ class Fabricator {
359
362
  await this.functionExecutor
360
363
  .run(async () => {
361
364
  const op = await gcfV2.deleteFunction(fnName);
362
- const pollerOptions = Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `delete-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name });
365
+ const pollerOptions = Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `delete-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name });
363
366
  await poller.pollOperation(pollerOptions);
364
367
  })
365
368
  .catch(rethrowAs(endpoint, "delete"));
@@ -471,7 +474,7 @@ class Fabricator {
471
474
  logOpStart(op, endpoint) {
472
475
  const runtime = (0, runtimes_1.getHumanFriendlyRuntimeName)(endpoint.runtime);
473
476
  const label = helper.getFunctionLabel(endpoint);
474
- utils.logBullet(`${clc.bold.cyan("functions:")} ${op} ${runtime} function ${clc.bold(label)}...`);
477
+ utils.logLabeledBullet("functions", `${op} ${runtime} function ${clc.bold(label)}...`);
475
478
  }
476
479
  logOpSuccess(op, endpoint) {
477
480
  const label = helper.getFunctionLabel(endpoint);
@@ -24,8 +24,18 @@ async function release(context, options, payload) {
24
24
  if (!payload.functions) {
25
25
  return;
26
26
  }
27
- const { wantBackend, haveBackend } = payload.functions;
28
- const plan = planner.createDeploymentPlan(wantBackend, haveBackend, context.filters);
27
+ if (!context.sources) {
28
+ return;
29
+ }
30
+ let plan = {};
31
+ for (const [codebase, { wantBackend, haveBackend }] of Object.entries(payload.functions)) {
32
+ plan = Object.assign(Object.assign({}, plan), planner.createDeploymentPlan({
33
+ codebase,
34
+ wantBackend,
35
+ haveBackend,
36
+ filters: context.filters,
37
+ }));
38
+ }
29
39
  const fnsToDelete = Object.values(plan)
30
40
  .map((regionalChanges) => regionalChanges.endpointsToDelete)
31
41
  .reduce(functional_1.reduceFlat, []);
@@ -44,15 +54,15 @@ async function release(context, options, payload) {
44
54
  const fab = new fabricator.Fabricator({
45
55
  functionExecutor,
46
56
  executor: new executor.QueueExecutor({}),
47
- sourceUrl: context.source.sourceUrl,
48
- storage: context.source.storage,
57
+ sources: context.sources,
49
58
  appEngineLocation: (0, functionsConfig_1.getAppEngineLocation)(context.firebaseConfig),
50
59
  });
51
60
  const summary = await fab.applyPlan(plan);
52
61
  await reporter.logAndTrackDeployStats(summary);
53
62
  reporter.printErrors(summary);
54
- printTriggerUrls(payload.functions.wantBackend);
55
- const haveEndpoints = backend.allEndpoints(payload.functions.wantBackend);
63
+ const wantBackend = backend.merge(...Object.values(payload.functions).map((p) => p.wantBackend));
64
+ printTriggerUrls(wantBackend);
65
+ const haveEndpoints = backend.allEndpoints(wantBackend);
56
66
  const deletedEndpoints = Object.values(plan)
57
67
  .map((r) => r.endpointsToDelete)
58
68
  .reduce(functional_1.reduceFlat, []);
@@ -48,20 +48,25 @@ function calculateUpdate(want, have) {
48
48
  return update;
49
49
  }
50
50
  exports.calculateUpdate = calculateUpdate;
51
- function createDeploymentPlan(want, have, filters, deleteAll) {
51
+ function createDeploymentPlan(args) {
52
+ let { wantBackend, haveBackend, codebase, filters, deleteAll } = args;
52
53
  let deployment = {};
53
- want = backend.matchingBackend(want, (endpoint) => {
54
+ wantBackend = backend.matchingBackend(wantBackend, (endpoint) => {
54
55
  return (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(endpoint, filters);
55
56
  });
56
- have = backend.matchingBackend(have, (endpoint) => {
57
- return (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(endpoint, filters);
57
+ const wantedEndpoint = backend.hasEndpoint(wantBackend);
58
+ haveBackend = backend.matchingBackend(haveBackend, (endpoint) => {
59
+ return wantedEndpoint(endpoint) || (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(endpoint, filters);
58
60
  });
59
- const regions = new Set([...Object.keys(want.endpoints), ...Object.keys(have.endpoints)]);
61
+ const regions = new Set([
62
+ ...Object.keys(wantBackend.endpoints),
63
+ ...Object.keys(haveBackend.endpoints),
64
+ ]);
60
65
  for (const region of regions) {
61
- const changesets = calculateChangesets(want.endpoints[region] || {}, have.endpoints[region] || {}, (e) => `${e.region}-${e.availableMemoryMb || "default"}`, deleteAll);
66
+ const changesets = calculateChangesets(wantBackend.endpoints[region] || {}, haveBackend.endpoints[region] || {}, (e) => `${codebase}-${e.region}-${e.availableMemoryMb || "default"}`, deleteAll);
62
67
  deployment = Object.assign(Object.assign({}, deployment), changesets);
63
68
  }
64
- if (upgradedToGCFv2WithoutSettingConcurrency(want, have)) {
69
+ if (upgradedToGCFv2WithoutSettingConcurrency(wantBackend, haveBackend)) {
65
70
  utils.logLabeledBullet("functions", "You are updating one or more functions to Google Cloud Functions v2, " +
66
71
  "which introduces support for concurrent execution. New functions " +
67
72
  "default to 80 concurrent executions, but existing functions keep the " +
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.secretsAreValid = exports.functionIdsAreValid = exports.functionsDirectoryExists = exports.endpointsAreValid = void 0;
3
+ exports.secretsAreValid = exports.functionIdsAreValid = exports.functionsDirectoryExists = exports.endpointsAreUnique = exports.endpointsAreValid = void 0;
4
4
  const path = require("path");
5
5
  const clc = require("cli-color");
6
6
  const error_1 = require("../../error");
@@ -39,6 +39,29 @@ function endpointsAreValid(wantBackend) {
39
39
  }
40
40
  }
41
41
  exports.endpointsAreValid = endpointsAreValid;
42
+ function endpointsAreUnique(backends) {
43
+ const endpointToCodebases = {};
44
+ for (const [codebase, b] of Object.entries(backends)) {
45
+ for (const endpoint of backend.allEndpoints(b)) {
46
+ const key = backend.functionName(endpoint);
47
+ const cs = endpointToCodebases[key] || new Set();
48
+ cs.add(codebase);
49
+ endpointToCodebases[key] = cs;
50
+ }
51
+ }
52
+ const conflicts = {};
53
+ for (const [fn, codebases] of Object.entries(endpointToCodebases)) {
54
+ if (codebases.size > 1) {
55
+ conflicts[fn] = Array.from(codebases);
56
+ }
57
+ }
58
+ if (Object.keys(conflicts).length === 0) {
59
+ return;
60
+ }
61
+ const msgs = Object.entries(conflicts).map(([fn, codebases]) => `${fn}: ${codebases.join(",")}`);
62
+ throw new error_1.FirebaseError("More than one codebase claims following functions:\n\t" + `${msgs.join("\n\t")}`);
63
+ }
64
+ exports.endpointsAreUnique = endpointsAreUnique;
42
65
  function functionsDirectoryExists(sourceDir, projectDir) {
43
66
  if (!fsutils.dirExistsSync(sourceDir)) {
44
67
  const sourceDirName = path.relative(projectDir, sourceDir);
@@ -40,10 +40,12 @@ async function convertConfig(context, payload, config, finalize) {
40
40
  return out;
41
41
  }
42
42
  const endpointBeingDeployed = (serviceId, region = "us-central1") => {
43
- var _a, _b, _c;
44
- const endpoint = (_c = (_b = (_a = payload.functions) === null || _a === void 0 ? void 0 : _a.wantBackend) === null || _b === void 0 ? void 0 : _b.endpoints[region]) === null || _c === void 0 ? void 0 : _c[serviceId];
45
- if (endpoint && (0, backend_1.isHttpsTriggered)(endpoint) && endpoint.platform === "gcfv2")
46
- return endpoint;
43
+ var _a;
44
+ for (const { wantBackend } of Object.values(payload.functions || {})) {
45
+ const endpoint = (_a = wantBackend === null || wantBackend === void 0 ? void 0 : wantBackend.endpoints[region]) === null || _a === void 0 ? void 0 : _a[serviceId];
46
+ if (endpoint && (0, backend_1.isHttpsTriggered)(endpoint) && endpoint.platform === "gcfv2")
47
+ return endpoint;
48
+ }
47
49
  return undefined;
48
50
  };
49
51
  const matchingEndpoint = async (serviceId, region = "us-central1") => {
@@ -268,16 +268,19 @@ async function startAll(options, showUI = true) {
268
268
  const emulatableBackends = [];
269
269
  const projectDir = (options.extDevDir || options.config.projectDir);
270
270
  if (shouldStart(options, types_1.Emulators.FUNCTIONS)) {
271
- const functionsCfg = (0, projectConfig_1.normalizeAndValidate)(options.config.src.functions)[0];
271
+ const functionsCfg = (0, projectConfig_1.normalizeAndValidate)(options.config.src.functions);
272
272
  utils.assertIsStringOrUndefined(options.extDevDir);
273
- const functionsDir = path.join(projectDir, functionsCfg.source);
274
- emulatableBackends.push({
275
- functionsDir,
276
- env: Object.assign({}, options.extDevEnv),
277
- secretEnv: [],
278
- predefinedTriggers: options.extDevTriggers,
279
- nodeMajorVersion: (0, functionsEmulatorUtils_1.parseRuntimeVersion)(options.extDevNodeVersion || functionsCfg.runtime),
280
- });
273
+ for (const cfg of functionsCfg) {
274
+ const functionsDir = path.join(projectDir, cfg.source);
275
+ emulatableBackends.push({
276
+ functionsDir,
277
+ codebase: cfg.codebase,
278
+ env: Object.assign({}, options.extDevEnv),
279
+ secretEnv: [],
280
+ predefinedTriggers: options.extDevTriggers,
281
+ nodeMajorVersion: (0, functionsEmulatorUtils_1.parseRuntimeVersion)(options.extDevNodeVersion || cfg.runtime),
282
+ });
283
+ }
281
284
  }
282
285
  if (shouldStart(options, types_1.Emulators.EXTENSIONS) && previews_1.previews.extensionsemulator) {
283
286
  const projectNumber = constants_1.Constants.isDemoProject(projectId)
@@ -177,7 +177,9 @@ class FunctionsEmulator {
177
177
  });
178
178
  return hub;
179
179
  }
180
- async invokeTrigger(backend, trigger, proto, runtimeOpts) {
180
+ async invokeTrigger(trigger, proto, runtimeOpts) {
181
+ const record = this.getTriggerRecordByKey(this.getTriggerKey(trigger));
182
+ const backend = record.backend;
181
183
  const bundleTemplate = this.getBaseBundle();
182
184
  const runtimeBundle = Object.assign(Object.assign({}, bundleTemplate), { proto });
183
185
  if (this.args.debugPort) {
@@ -284,6 +286,9 @@ class FunctionsEmulator {
284
286
  const discoveredBackend = await runtimeDelegate.discoverSpec(runtimeConfig, Object.assign(Object.assign(Object.assign(Object.assign({}, this.getSystemEnvs()), this.getEmulatorEnvs()), { FIREBASE_CONFIG: this.getFirebaseConfig() }), emulatableBackend.env));
285
287
  const endpoints = backend.allEndpoints(discoveredBackend);
286
288
  (0, functionsEmulatorShared_1.prepareEndpoints)(endpoints);
289
+ for (const e of endpoints) {
290
+ e.codebase = emulatableBackend.codebase;
291
+ }
287
292
  triggerDefinitions = (0, functionsEmulatorShared_1.emulatedFunctionsFromEndpoints)(endpoints);
288
293
  }
289
294
  const toSetup = triggerDefinitions.filter((definition) => {
@@ -579,6 +584,7 @@ class FunctionsEmulator {
579
584
  };
580
585
  }
581
586
  setTriggersForTesting(triggers, backend) {
587
+ this.triggers = {};
582
588
  triggers.forEach((def) => this.addTriggerRecord(def, { backend, ignored: false }));
583
589
  }
584
590
  getBaseBundle() {
@@ -862,7 +868,7 @@ class FunctionsEmulator {
862
868
  }
863
869
  const trigger = record.def;
864
870
  const service = (0, functionsEmulatorShared_1.getFunctionService)(trigger);
865
- const worker = await this.invokeTrigger(record.backend, trigger, proto);
871
+ const worker = await this.invokeTrigger(trigger, proto);
866
872
  return new Promise((resolve, reject) => {
867
873
  if (projectId !== this.args.projectId) {
868
874
  if (service !== constants_1.Constants.SERVICE_REALTIME_DATABASE) {
@@ -949,7 +955,7 @@ class FunctionsEmulator {
949
955
  req.headers[functionsEmulatorShared_1.HttpConstants.CALLABLE_AUTH_HEADER] = encodeURIComponent(JSON.stringify(contextAuth));
950
956
  }
951
957
  }
952
- const worker = await this.invokeTrigger(record.backend, trigger);
958
+ const worker = await this.invokeTrigger(trigger);
953
959
  worker.onLogs((el) => {
954
960
  if (el.level === "FATAL") {
955
961
  res.status(500).send(el.text);
@@ -66,6 +66,7 @@ function emulatedFunctionsFromEndpoints(endpoints) {
66
66
  region: endpoint.region,
67
67
  name: endpoint.id,
68
68
  id: `${endpoint.region}-${endpoint.id}`,
69
+ codebase: endpoint.codebase,
69
70
  };
70
71
  (0, proto_1.copyIfPresent)(def, endpoint, "availableMemoryMb", "labels", "timeoutSeconds", "platform", "secretEnvironmentVariables");
71
72
  if (backend.isHttpsTriggered(endpoint)) {
@@ -279,7 +280,7 @@ function toBackendInfo(e, cf3Triggers) {
279
280
  extension: e.extension,
280
281
  extensionVersion: extensionVersion,
281
282
  extensionSpec: extensionSpec,
282
- functionTriggers: (_b = e.predefinedTriggers) !== null && _b !== void 0 ? _b : cf3Triggers,
283
+ functionTriggers: (_b = e.predefinedTriggers) !== null && _b !== void 0 ? _b : cf3Triggers.filter((t) => t.codebase === e.codebase),
283
284
  }));
284
285
  }
285
286
  exports.toBackendInfo = toBackendInfo;
@@ -7,9 +7,8 @@ const utils = require("../utils");
7
7
  const logger_1 = require("../logger");
8
8
  const error_1 = require("../error");
9
9
  class FunctionsEmulatorShell {
10
- constructor(emu, backend) {
10
+ constructor(emu) {
11
11
  this.emu = emu;
12
- this.backend = backend;
13
12
  this.urls = {};
14
13
  this.triggers = emu.getTriggerDefinitions();
15
14
  this.emulatedFunctions = this.triggers.map((t) => t.id);
@@ -42,7 +41,7 @@ class FunctionsEmulatorShell {
42
41
  auth: opts.auth,
43
42
  data,
44
43
  };
45
- this.emu.invokeTrigger(this.backend, trigger, proto);
44
+ this.emu.invokeTrigger(trigger, proto);
46
45
  }
47
46
  getTrigger(name) {
48
47
  const result = this.triggers.find((trigger) => {
@@ -44,14 +44,7 @@ class PubsubEmulator {
44
44
  getName() {
45
45
  return types_1.Emulators.PUBSUB;
46
46
  }
47
- async addTrigger(topicName, triggerKey, signatureType) {
48
- this.logger.logLabeled("DEBUG", "pubsub", `addTrigger(${topicName}, ${triggerKey}, ${signatureType})`);
49
- const triggers = this.triggersForTopic.get(topicName) || [];
50
- if (triggers.some((t) => t.triggerKey === triggerKey) &&
51
- this.subscriptionForTopic.has(topicName)) {
52
- this.logger.logLabeled("DEBUG", "pubsub", "Trigger already exists");
53
- return;
54
- }
47
+ async maybeCreateTopicAndSub(topicName) {
55
48
  const topic = this.pubsub.topic(topicName);
56
49
  try {
57
50
  this.logger.logLabeled("DEBUG", "pubsub", `Creating topic: ${topicName}`);
@@ -74,7 +67,7 @@ class PubsubEmulator {
74
67
  catch (e) {
75
68
  if (e && e.code === 6) {
76
69
  this.logger.logLabeled("DEBUG", "pubsub", `Sub for ${topicName} exists`);
77
- sub = topic.subscription(`emulator-sub-${topicName}`);
70
+ sub = topic.subscription(subName);
78
71
  }
79
72
  else {
80
73
  throw new error_1.FirebaseError(`Could not create sub ${subName}`, { original: e });
@@ -83,6 +76,17 @@ class PubsubEmulator {
83
76
  sub.on("message", (message) => {
84
77
  this.onMessage(topicName, message);
85
78
  });
79
+ return sub;
80
+ }
81
+ async addTrigger(topicName, triggerKey, signatureType) {
82
+ this.logger.logLabeled("DEBUG", "pubsub", `addTrigger(${topicName}, ${triggerKey}, ${signatureType})`);
83
+ const sub = await this.maybeCreateTopicAndSub(topicName);
84
+ const triggers = this.triggersForTopic.get(topicName) || [];
85
+ if (triggers.some((t) => t.triggerKey === triggerKey) &&
86
+ this.subscriptionForTopic.has(topicName)) {
87
+ this.logger.logLabeled("DEBUG", "pubsub", "Trigger already exists");
88
+ return;
89
+ }
86
90
  triggers.push({ triggerKey, signatureType });
87
91
  this.triggersForTopic.set(topicName, triggers);
88
92
  this.subscriptionForTopic.set(topicName, sub);
@@ -28,7 +28,7 @@ async function check(projectId, apiName, prefix, silent = false) {
28
28
  exports.check = check;
29
29
  async function enable(projectId, apiName) {
30
30
  try {
31
- await apiClient.post(`/projects/${projectId}/services/${apiName}:enable`, {
31
+ await apiClient.post(`/projects/${projectId}/services/${apiName}:enable`, undefined, {
32
32
  skipLog: { resBody: true },
33
33
  });
34
34
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeAndValidate = exports.validate = exports.normalize = exports.DEFAULT_CODEBASE = void 0;
3
+ exports.configForCodebase = exports.normalizeAndValidate = exports.validate = exports.normalize = exports.DEFAULT_CODEBASE = void 0;
4
4
  const error_1 = require("../error");
5
5
  exports.DEFAULT_CODEBASE = "default";
6
6
  function normalize(config) {
@@ -40,16 +40,21 @@ function assertUnique(config, property) {
40
40
  }
41
41
  }
42
42
  function validate(config) {
43
- if (config.length > 1) {
44
- throw new error_1.FirebaseError("More than one functions.source detected in firebase.json.");
45
- }
46
- const validated = validateSingle(config[0]);
47
- assertUnique([validated], "source");
48
- assertUnique([validated], "codebase");
49
- return [validated];
43
+ const validated = config.map((cfg) => validateSingle(cfg));
44
+ assertUnique(validated, "source");
45
+ assertUnique(validated, "codebase");
46
+ return validated;
50
47
  }
51
48
  exports.validate = validate;
52
49
  function normalizeAndValidate(config) {
53
50
  return validate(normalize(config));
54
51
  }
55
52
  exports.normalizeAndValidate = normalizeAndValidate;
53
+ function configForCodebase(config, codebase) {
54
+ const codebaseCfg = config.find((c) => c.codebase === codebase);
55
+ if (!codebaseCfg) {
56
+ throw new error_1.FirebaseError(`No functions config found for codebase ${codebase}`);
57
+ }
58
+ return codebaseCfg;
59
+ }
60
+ exports.configForCodebase = configForCodebase;
@@ -55,7 +55,7 @@ const actionFunction = async (options) => {
55
55
  })
56
56
  .then(() => {
57
57
  const instance = serveFunctions.get();
58
- const emulator = new shell.FunctionsEmulatorShell(instance, serveFunctions.getBackend());
58
+ const emulator = new shell.FunctionsEmulatorShell(instance);
59
59
  if (emulator.emulatedFunctions && emulator.emulatedFunctions.length === 0) {
60
60
  logger_1.logger.info("No functions emulated.");
61
61
  process.exit();
@@ -280,6 +280,7 @@ function endpointFromFunction(gcfFunction) {
280
280
  }
281
281
  exports.endpointFromFunction = endpointFromFunction;
282
282
  function functionFromEndpoint(endpoint, sourceUploadUrl) {
283
+ var _a;
283
284
  if (endpoint.platform !== "gcfv1") {
284
285
  throw new error_1.FirebaseError("Trying to create a v1 CloudFunction with v2 API. This should never happen");
285
286
  }
@@ -334,7 +335,13 @@ function functionFromEndpoint(endpoint, sourceUploadUrl) {
334
335
  proto.renameIfPresent(gcfFunction, endpoint.vpc, "vpcConnector", "connector");
335
336
  proto.renameIfPresent(gcfFunction, endpoint.vpc, "vpcConnectorEgressSettings", "egressSettings");
336
337
  }
337
- gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { [exports.CODEBASE_LABEL]: endpoint.codebase || projectConfig.DEFAULT_CODEBASE });
338
+ const codebase = endpoint.codebase || projectConfig.DEFAULT_CODEBASE;
339
+ if (codebase !== projectConfig.DEFAULT_CODEBASE) {
340
+ gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { [exports.CODEBASE_LABEL]: codebase });
341
+ }
342
+ else {
343
+ (_a = gcfFunction.labels) === null || _a === void 0 ? true : delete _a[exports.CODEBASE_LABEL];
344
+ }
338
345
  return gcfFunction;
339
346
  }
340
347
  exports.functionFromEndpoint = functionFromEndpoint;
@@ -157,6 +157,7 @@ async function deleteFunction(cloudFunction) {
157
157
  }
158
158
  exports.deleteFunction = deleteFunction;
159
159
  function functionFromEndpoint(endpoint, source) {
160
+ var _a;
160
161
  if (endpoint.platform !== "gcfv2") {
161
162
  throw new error_1.FirebaseError("Trying to create a v2 CloudFunction with v1 API. This should never happen");
162
163
  }
@@ -223,7 +224,13 @@ function functionFromEndpoint(endpoint, source) {
223
224
  else if (backend.isBlockingTriggered(endpoint)) {
224
225
  gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { [exports.BLOCKING_LABEL]: BLOCKING_EVENT_TO_LABEL_KEY[endpoint.blockingTrigger.eventType] });
225
226
  }
226
- gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { [exports.CODEBASE_LABEL]: endpoint.codebase || projectConfig.DEFAULT_CODEBASE });
227
+ const codebase = endpoint.codebase || projectConfig.DEFAULT_CODEBASE;
228
+ if (codebase !== projectConfig.DEFAULT_CODEBASE) {
229
+ gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { [exports.CODEBASE_LABEL]: codebase });
230
+ }
231
+ else {
232
+ (_a = gcfFunction.labels) === null || _a === void 0 ? true : delete _a[exports.CODEBASE_LABEL];
233
+ }
227
234
  return gcfFunction;
228
235
  }
229
236
  exports.functionFromEndpoint = functionFromEndpoint;
@@ -10,28 +10,29 @@ const auth_1 = require("../auth");
10
10
  const projectConfig = require("../functions/projectConfig");
11
11
  const utils = require("../utils");
12
12
  class FunctionsServer {
13
- constructor() {
14
- this.emulatorServer = undefined;
15
- this.backend = undefined;
16
- }
17
13
  assertServer() {
18
- if (!this.emulatorServer || !this.backend) {
14
+ if (!this.emulatorServer || !this.backends) {
19
15
  throw new Error("Must call start() before calling any other operation!");
20
16
  }
21
17
  }
22
18
  async start(options, partialArgs) {
23
19
  const projectId = (0, projectUtils_1.needProjectId)(options);
24
- const config = projectConfig.normalizeAndValidate(options.config.src.functions)[0];
25
- const functionsDir = path.join(options.config.projectDir, config.source);
20
+ const config = projectConfig.normalizeAndValidate(options.config.src.functions);
21
+ const backends = [];
22
+ for (const cfg of config) {
23
+ const functionsDir = path.join(options.config.projectDir, cfg.source);
24
+ const nodeMajorVersion = (0, functionsEmulatorUtils_1.parseRuntimeVersion)(cfg.runtime);
25
+ backends.push({
26
+ functionsDir,
27
+ codebase: cfg.codebase,
28
+ nodeMajorVersion,
29
+ env: {},
30
+ secretEnv: [],
31
+ });
32
+ }
33
+ this.backends = backends;
26
34
  const account = (0, auth_1.getProjectDefaultAccount)(options.config.projectDir);
27
- const nodeMajorVersion = (0, functionsEmulatorUtils_1.parseRuntimeVersion)(config.runtime);
28
- this.backend = {
29
- functionsDir,
30
- nodeMajorVersion,
31
- env: {},
32
- secretEnv: [],
33
- };
34
- const args = Object.assign({ projectId, projectDir: options.config.projectDir, emulatableBackends: [this.backend], projectAlias: options.projectAlias, account }, partialArgs);
35
+ const args = Object.assign({ projectId, projectDir: options.config.projectDir, emulatableBackends: this.backends, projectAlias: options.projectAlias, account }, partialArgs);
35
36
  if (options.host) {
36
37
  utils.assertIsStringOrUndefined(options.host);
37
38
  args.host = options.host;
@@ -59,10 +60,6 @@ class FunctionsServer {
59
60
  this.assertServer();
60
61
  await this.emulatorServer.stop();
61
62
  }
62
- getBackend() {
63
- this.assertServer();
64
- return this.backend;
65
- }
66
63
  get() {
67
64
  this.assertServer();
68
65
  return this.emulatorServer.get();
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "10.7.0",
3
+ "version": "10.7.1",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "firebase-tools",
9
- "version": "10.7.0",
9
+ "version": "10.7.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@google-cloud/pubsub": "^2.18.4",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "10.7.0",
3
+ "version": "10.7.1",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {