firebase-tools 11.2.1 → 11.4.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 (40) hide show
  1. package/lib/apiv2.js +5 -0
  2. package/lib/checkValidTargetFilters.js +3 -2
  3. package/lib/commands/ext-export.js +2 -0
  4. package/lib/database/rulesConfig.js +35 -8
  5. package/lib/deploy/extensions/planner.js +1 -0
  6. package/lib/deploy/extensions/prepare.js +18 -1
  7. package/lib/deploy/extensions/release.js +4 -0
  8. package/lib/deploy/functions/build.js +43 -52
  9. package/lib/deploy/functions/params.js +189 -0
  10. package/lib/deploy/functions/prepare.js +1 -1
  11. package/lib/deploy/functions/release/index.js +4 -0
  12. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +3 -0
  13. package/lib/deploy/functions/runtimes/node/parseTriggers.js +1 -1
  14. package/lib/deploy/hosting/convertConfig.js +76 -16
  15. package/lib/deploy/index.js +1 -1
  16. package/lib/deploy/lifecycleHooks.js +1 -1
  17. package/lib/deploy/storage/prepare.js +29 -6
  18. package/lib/downloadUtils.js +1 -1
  19. package/lib/emulator/controller.js +0 -1
  20. package/lib/emulator/downloadableEmulators.js +9 -9
  21. package/lib/emulator/functionsEmulator.js +9 -1
  22. package/lib/emulator/functionsEmulatorRuntime.js +68 -58
  23. package/lib/emulator/functionsRuntimeWorker.js +35 -9
  24. package/lib/emulator/storage/apis/gcloud.js +1 -1
  25. package/lib/extensions/etags.js +28 -0
  26. package/lib/extensions/warnings.js +7 -1
  27. package/lib/functions/env.js +51 -2
  28. package/lib/functionsConfig.js +1 -1
  29. package/lib/gcp/iam.js +20 -17
  30. package/lib/gcp/rules.js +2 -3
  31. package/lib/gcp/runtimeconfig.js +1 -1
  32. package/lib/hosting/api.js +9 -11
  33. package/lib/hosting/expireUtils.js +2 -2
  34. package/lib/management/projects.js +6 -7
  35. package/lib/profileReport.js +16 -14
  36. package/lib/rc.js +14 -10
  37. package/lib/requireConfig.js +6 -6
  38. package/lib/rulesDeploy.js +1 -1
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +2 -1
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectEtagChanges = exports.saveEtags = void 0;
4
+ function saveEtags(rc, projectId, instances) {
5
+ rc.setEtags(projectId, "extensionInstances", etagsMap(instances));
6
+ }
7
+ exports.saveEtags = saveEtags;
8
+ function detectEtagChanges(rc, projectId, instances) {
9
+ const lastDeployedEtags = rc.getEtags(projectId).extensionInstances;
10
+ const currentEtags = etagsMap(instances);
11
+ if (!lastDeployedEtags || !Object.keys(lastDeployedEtags).length) {
12
+ return [];
13
+ }
14
+ const changedExtensionInstances = Object.entries(lastDeployedEtags)
15
+ .filter(([instanceName, etag]) => etag !== currentEtags[instanceName])
16
+ .map((i) => i[0]);
17
+ const newExtensionInstances = Object.keys(currentEtags).filter((instanceName) => !lastDeployedEtags[instanceName]);
18
+ return newExtensionInstances.concat(changedExtensionInstances);
19
+ }
20
+ exports.detectEtagChanges = detectEtagChanges;
21
+ function etagsMap(instances) {
22
+ return instances.reduce((acc, i) => {
23
+ if (i.etag) {
24
+ acc[i.instanceId] = i.etag;
25
+ }
26
+ return acc;
27
+ }, {});
28
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.paramsFlagDeprecationWarning = exports.displayWarningsForDeploy = exports.displayWarningPrompts = void 0;
3
+ exports.outOfBandChangesWarning = exports.paramsFlagDeprecationWarning = exports.displayWarningsForDeploy = exports.displayWarningPrompts = void 0;
4
4
  const { marked } = require("marked");
5
5
  const clc = require("cli-color");
6
6
  const types_1 = require("./types");
@@ -75,3 +75,9 @@ function paramsFlagDeprecationWarning() {
75
75
  "See https://firebase.google.com/docs/extensions/manifest for more details");
76
76
  }
77
77
  exports.paramsFlagDeprecationWarning = paramsFlagDeprecationWarning;
78
+ function outOfBandChangesWarning(instanceIds) {
79
+ logger_1.logger.warn("The following instances may have been changed in the Firebase console or by another machine since the last deploy from this machine.\n\t" +
80
+ clc.bold(instanceIds.join("\n\t")) +
81
+ "\nIf you proceed with this deployment, those changes will be overwritten. To avoid this, run `firebase ext:export` to sync these changes to your local extensions manifest.");
82
+ }
83
+ exports.outOfBandChangesWarning = outOfBandChangesWarning;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadFirebaseEnvs = exports.loadUserEnvs = exports.hasUserEnvs = exports.parseStrict = exports.validateKey = exports.KeyValidationError = exports.parse = void 0;
3
+ exports.loadFirebaseEnvs = exports.loadUserEnvs = exports.writeUserEnvs = exports.hasUserEnvs = exports.parseStrict = exports.validateKey = exports.KeyValidationError = exports.parse = void 0;
4
4
  const clc = require("cli-color");
5
5
  const fs = require("fs");
6
6
  const path = require("path");
@@ -51,6 +51,17 @@ const ESCAPE_SEQUENCES_TO_CHARACTERS = {
51
51
  "\\'": "'",
52
52
  '\\"': '"',
53
53
  };
54
+ const ALL_ESCAPE_SEQUENCES_RE = /\\[nrtv\\'"]/g;
55
+ const CHARACTERS_TO_ESCAPE_SEQUENCES = {
56
+ "\n": "\\n",
57
+ "\r": "\\r",
58
+ "\t": "\\t",
59
+ "\v": "\\v",
60
+ "\\": "\\\\",
61
+ "'": "\\'",
62
+ '"': '\\"',
63
+ };
64
+ const ALL_ESCAPABLE_CHARACTERS_RE = /[\n\r\t\v\\'"]/g;
54
65
  function parse(data) {
55
66
  const envs = {};
56
67
  const errors = [];
@@ -63,7 +74,7 @@ function parse(data) {
63
74
  if ((quotesMatch = /^(["'])(.*)\1$/ms.exec(v)) != null) {
64
75
  v = quotesMatch[2];
65
76
  if (quotesMatch[1] === '"') {
66
- v = v.replace(/\\[nrtv\\'"]/g, (match) => ESCAPE_SEQUENCES_TO_CHARACTERS[match]);
77
+ v = v.replace(ALL_ESCAPE_SEQUENCES_RE, (match) => ESCAPE_SEQUENCES_TO_CHARACTERS[match]);
67
78
  }
68
79
  }
69
80
  envs[k] = v;
@@ -145,6 +156,44 @@ function hasUserEnvs({ functionsSource, projectId, projectAlias, isEmulator, })
145
156
  return findEnvfiles(functionsSource, projectId, projectAlias, isEmulator).length > 0;
146
157
  }
147
158
  exports.hasUserEnvs = hasUserEnvs;
159
+ function writeUserEnvs(toWrite, envOpts) {
160
+ if (Object.keys(toWrite).length === 0) {
161
+ return;
162
+ }
163
+ const { functionsSource, projectId, projectAlias, isEmulator } = envOpts;
164
+ let envFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator);
165
+ if (envFiles.length === 0) {
166
+ envFiles = [createEnvFile(envOpts)];
167
+ }
168
+ const currentEnvs = loadUserEnvs(envOpts);
169
+ for (const k of Object.keys(toWrite)) {
170
+ validateKey(k);
171
+ if (currentEnvs.hasOwnProperty(k)) {
172
+ throw new error_1.FirebaseError(`Attempted to write param-defined key ${k} to .env files, but it was already defined.`);
173
+ }
174
+ }
175
+ const mostSpecificEnv = path.join(functionsSource, envFiles[envFiles.length - 1]);
176
+ (0, utils_1.logBullet)(clc.cyan.bold("functions: ") + `Writing new parameter values to disk: ${mostSpecificEnv}`);
177
+ for (const k of Object.keys(toWrite)) {
178
+ fs.appendFileSync(mostSpecificEnv, formatUserEnvForWrite(k, toWrite[k]));
179
+ }
180
+ }
181
+ exports.writeUserEnvs = writeUserEnvs;
182
+ function createEnvFile(envOpts) {
183
+ const fileToWrite = envOpts.isEmulator
184
+ ? FUNCTIONS_EMULATOR_DOTENV
185
+ : `.env.${envOpts.projectAlias || envOpts.projectId}`;
186
+ logger_1.logger.debug(`Creating ${fileToWrite}...`);
187
+ fs.writeFileSync(path.join(envOpts.functionsSource, fileToWrite), "", { flag: "wx" });
188
+ return fileToWrite;
189
+ }
190
+ function formatUserEnvForWrite(key, value) {
191
+ const escapedValue = value.replace(ALL_ESCAPABLE_CHARACTERS_RE, (match) => CHARACTERS_TO_ESCAPE_SEQUENCES[match]);
192
+ if (escapedValue !== value) {
193
+ return `${key}="${escapedValue}"\n`;
194
+ }
195
+ return `${key}=${escapedValue}\n`;
196
+ }
148
197
  function loadUserEnvs({ functionsSource, projectId, projectAlias, isEmulator, }) {
149
198
  var _a;
150
199
  const envFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator);
@@ -70,7 +70,7 @@ async function setVariablesRecursive(projectId, configId, varPath, val) {
70
70
  catch (e) {
71
71
  }
72
72
  }
73
- if (_.isPlainObject(parsed)) {
73
+ if (typeof parsed === "object" && parsed !== null) {
74
74
  return Promise.all(Object.entries(parsed).map(([key, item]) => {
75
75
  const newVarPath = varPath ? [varPath, key].join("/") : key;
76
76
  return setVariablesRecursive(projectId, configId, newVarPath, item);
package/lib/gcp/iam.js CHANGED
@@ -2,11 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.testIamPermissions = exports.testResourceIamPermissions = exports.getRole = exports.deleteServiceAccount = exports.createServiceAccountKey = exports.getServiceAccount = exports.createServiceAccount = void 0;
4
4
  const api_1 = require("../api");
5
- const lodash_1 = require("lodash");
6
5
  const logger_1 = require("../logger");
7
6
  const apiv2_1 = require("../apiv2");
8
- const API_VERSION = "v1";
9
- const apiClient = new apiv2_1.Client({ urlPrefix: api_1.iamOrigin, apiVersion: API_VERSION });
7
+ const apiClient = new apiv2_1.Client({ urlPrefix: api_1.iamOrigin, apiVersion: "v1" });
10
8
  async function createServiceAccount(projectId, accountId, description, displayName) {
11
9
  const response = await apiClient.post(`/projects/${projectId}/serviceAccounts`, {
12
10
  accountId,
@@ -31,8 +29,8 @@ async function createServiceAccountKey(projectId, serviceAccountName) {
31
29
  return response.body;
32
30
  }
33
31
  exports.createServiceAccountKey = createServiceAccountKey;
34
- function deleteServiceAccount(projectId, accountEmail) {
35
- return apiClient.delete(`/projects/${projectId}/serviceAccounts/${accountEmail}`, {
32
+ async function deleteServiceAccount(projectId, accountEmail) {
33
+ await apiClient.delete(`/projects/${projectId}/serviceAccounts/${accountEmail}`, {
36
34
  resolveOnHTTPError: true,
37
35
  });
38
36
  }
@@ -44,25 +42,30 @@ async function getRole(role) {
44
42
  return response.body;
45
43
  }
46
44
  exports.getRole = getRole;
47
- async function testResourceIamPermissions(origin, apiVersion, resourceName, permissions) {
45
+ async function testResourceIamPermissions(origin, apiVersion, resourceName, permissions, quotaUser = "") {
48
46
  const localClient = new apiv2_1.Client({ urlPrefix: origin, apiVersion });
49
47
  if (process.env.FIREBASE_SKIP_INFORMATIONAL_IAM) {
50
- logger_1.logger.debug("[iam] skipping informational check of permissions", JSON.stringify(permissions), "on resource", resourceName);
51
- return { allowed: permissions, missing: [], passed: true };
48
+ logger_1.logger.debug(`[iam] skipping informational check of permissions ${JSON.stringify(permissions)} on resource ${resourceName}`);
49
+ return { allowed: Array.from(permissions).sort(), missing: [], passed: true };
50
+ }
51
+ const headers = {};
52
+ if (quotaUser) {
53
+ headers["x-goog-quota-user"] = quotaUser;
54
+ }
55
+ const response = await localClient.post(`/${resourceName}:testIamPermissions`, { permissions }, { headers });
56
+ const allowed = new Set(response.body.permissions || []);
57
+ const missing = new Set(permissions);
58
+ for (const p of allowed) {
59
+ missing.delete(p);
52
60
  }
53
- const response = await localClient.post(`/${resourceName}:testIamPermissions`, {
54
- permissions,
55
- });
56
- const allowed = (response.body.permissions || []).sort();
57
- const missing = (0, lodash_1.difference)(permissions, allowed);
58
61
  return {
59
- allowed,
60
- missing,
61
- passed: missing.length === 0,
62
+ allowed: Array.from(allowed).sort(),
63
+ missing: Array.from(missing).sort(),
64
+ passed: missing.size === 0,
62
65
  };
63
66
  }
64
67
  exports.testResourceIamPermissions = testResourceIamPermissions;
65
68
  async function testIamPermissions(projectId, permissions) {
66
- return testResourceIamPermissions(api_1.resourceManagerOrigin, "v1", `projects/${projectId}`, permissions);
69
+ return testResourceIamPermissions(api_1.resourceManagerOrigin, "v1", `projects/${projectId}`, permissions, `projects/${projectId}`);
67
70
  }
68
71
  exports.testIamPermissions = testIamPermissions;
package/lib/gcp/rules.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.testRuleset = exports.updateOrCreateRelease = exports.updateRelease = exports.createRelease = exports.createRuleset = exports.deleteRuleset = exports.getRulesetId = exports.listAllRulesets = exports.listRulesets = exports.getRulesetContent = exports.listAllReleases = exports.listReleases = exports.getLatestRulesetName = void 0;
4
- const _ = require("lodash");
5
4
  const api_1 = require("../api");
6
5
  const apiv2_1 = require("../apiv2");
7
6
  const logger_1 = require("../logger");
@@ -51,7 +50,7 @@ async function listAllReleases(projectId) {
51
50
  }
52
51
  pageToken = response.nextPageToken;
53
52
  } while (pageToken);
54
- return _.orderBy(releases, ["createTime"], ["desc"]);
53
+ return releases.sort((a, b) => b.createTime.localeCompare(a.createTime));
55
54
  }
56
55
  exports.listAllReleases = listAllReleases;
57
56
  async function getRulesetContent(name) {
@@ -90,7 +89,7 @@ async function listAllRulesets(projectId) {
90
89
  }
91
90
  pageToken = response.nextPageToken;
92
91
  } while (pageToken);
93
- return _.orderBy(rulesets, ["createTime"], ["desc"]);
92
+ return rulesets.sort((a, b) => b.createTime.localeCompare(a.createTime));
94
93
  }
95
94
  exports.listAllRulesets = listAllRulesets;
96
95
  function getRulesetId(ruleset) {
@@ -48,7 +48,7 @@ function listVariables(configPath) {
48
48
  retryCodes: [500, 503],
49
49
  })
50
50
  .then((resp) => {
51
- return Promise.resolve(resp.body.variables);
51
+ return Promise.resolve(resp.body.variables || []);
52
52
  });
53
53
  }
54
54
  function getVariable(varPath) {
@@ -43,7 +43,7 @@ async function getChannel(project = "-", site, channelId) {
43
43
  return res.body;
44
44
  }
45
45
  catch (e) {
46
- if (e.status === 404) {
46
+ if (e instanceof error_1.FirebaseError && e.status === 404) {
47
47
  return null;
48
48
  }
49
49
  throw e;
@@ -51,23 +51,22 @@ async function getChannel(project = "-", site, channelId) {
51
51
  }
52
52
  exports.getChannel = getChannel;
53
53
  async function listChannels(project = "-", site) {
54
- var _a, _b;
55
54
  const channels = [];
56
55
  let nextPageToken = "";
57
56
  for (;;) {
58
57
  try {
59
58
  const res = await apiClient.get(`/projects/${project}/sites/${site}/channels`, { queryParams: { pageToken: nextPageToken, pageSize: 10 } });
60
- const c = (_a = res.body) === null || _a === void 0 ? void 0 : _a.channels;
59
+ const c = res.body.channels;
61
60
  if (c) {
62
61
  channels.push(...c);
63
62
  }
64
- nextPageToken = ((_b = res.body) === null || _b === void 0 ? void 0 : _b.nextPageToken) || "";
63
+ nextPageToken = res.body.nextPageToken || "";
65
64
  if (!nextPageToken) {
66
65
  return channels;
67
66
  }
68
67
  }
69
68
  catch (e) {
70
- if (e.status === 404) {
69
+ if (e instanceof error_1.FirebaseError && e.status === 404) {
71
70
  throw new error_1.FirebaseError(`could not find channels for site "${site}"`, {
72
71
  original: e,
73
72
  });
@@ -116,23 +115,22 @@ async function createRelease(site, channel, version) {
116
115
  }
117
116
  exports.createRelease = createRelease;
118
117
  async function listSites(project) {
119
- var _a, _b;
120
118
  const sites = [];
121
119
  let nextPageToken = "";
122
120
  for (;;) {
123
121
  try {
124
122
  const res = await apiClient.get(`/projects/${project}/sites`, { queryParams: { pageToken: nextPageToken, pageSize: 10 } });
125
- const c = (_a = res.body) === null || _a === void 0 ? void 0 : _a.sites;
123
+ const c = res.body.sites;
126
124
  if (c) {
127
125
  sites.push(...c);
128
126
  }
129
- nextPageToken = ((_b = res.body) === null || _b === void 0 ? void 0 : _b.nextPageToken) || "";
127
+ nextPageToken = res.body.nextPageToken || "";
130
128
  if (!nextPageToken) {
131
129
  return sites;
132
130
  }
133
131
  }
134
132
  catch (e) {
135
- if (e.status === 404) {
133
+ if (e instanceof error_1.FirebaseError && e.status === 404) {
136
134
  throw new error_1.FirebaseError(`could not find sites for project "${project}"`, {
137
135
  original: e,
138
136
  });
@@ -148,7 +146,7 @@ async function getSite(project, site) {
148
146
  return res.body;
149
147
  }
150
148
  catch (e) {
151
- if (e.status === 404) {
149
+ if (e instanceof error_1.FirebaseError && e.status === 404) {
152
150
  throw new error_1.FirebaseError(`could not find site "${site}" for project "${project}"`, {
153
151
  original: e,
154
152
  });
@@ -158,7 +156,7 @@ async function getSite(project, site) {
158
156
  }
159
157
  exports.getSite = getSite;
160
158
  async function createSite(project, site, appId = "") {
161
- const res = await apiClient.post(`/projects/${project}/sites`, { appId: appId }, { queryParams: { site_id: site } });
159
+ const res = await apiClient.post(`/projects/${project}/sites`, { appId: appId }, { queryParams: { siteId: site } });
162
160
  return res.body;
163
161
  }
164
162
  exports.createSite = createSite;
@@ -16,8 +16,8 @@ const DURATIONS = {
16
16
  };
17
17
  exports.MAX_DURATION = 30 * Duration.DAY;
18
18
  exports.DEFAULT_DURATION = 7 * Duration.DAY;
19
- function calculateChannelExpireTTL(flag) {
20
- const match = exports.DURATION_REGEX.exec(flag || "");
19
+ function calculateChannelExpireTTL(flag = "") {
20
+ const match = exports.DURATION_REGEX.exec(flag);
21
21
  if (!match) {
22
22
  throw new error_1.FirebaseError(`"expires" flag must be a duration string (e.g. 24h or 7d) at most 30d`);
23
23
  }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getFirebaseProject = exports.listFirebaseProjects = exports.getAvailableCloudProjectPage = exports.getFirebaseProjectPage = exports.addFirebaseToCloudProject = exports.createCloudProject = exports.promptAvailableProjectId = exports.getOrPromptProject = exports.addFirebaseToCloudProjectAndLog = exports.createFirebaseProjectAndLog = exports.PROJECTS_CREATE_QUESTIONS = exports.ProjectParentResourceType = void 0;
4
- const _ = require("lodash");
5
4
  const clc = require("cli-color");
6
5
  const ora = require("ora");
7
6
  const apiv2_1 = require("../apiv2");
@@ -110,15 +109,15 @@ async function selectProjectByPrompting() {
110
109
  return await getFirebaseProject(projectId);
111
110
  }
112
111
  async function selectProjectFromList(projects = []) {
113
- let choices = projects
112
+ const choices = projects
114
113
  .filter((p) => !!p)
115
114
  .map((p) => {
116
115
  return {
117
116
  name: p.projectId + (p.displayName ? ` (${p.displayName})` : ""),
118
117
  value: p.projectId,
119
118
  };
120
- });
121
- choices = _.orderBy(choices, ["name"], ["asc"]);
119
+ })
120
+ .sort((a, b) => a.name.localeCompare(b.name));
122
121
  if (choices.length >= 25) {
123
122
  utils.logBullet(`Don't want to scroll through all your projects? If you know your project ID, ` +
124
123
  `you can initialize it directly using ${clc.bold("firebase init --project <project_id>")}.\n`);
@@ -151,7 +150,7 @@ async function promptAvailableProjectId() {
151
150
  });
152
151
  }
153
152
  else {
154
- let choices = projects
153
+ const choices = projects
155
154
  .filter((p) => !!p)
156
155
  .map((p) => {
157
156
  const projectId = getProjectId(p);
@@ -159,8 +158,8 @@ async function promptAvailableProjectId() {
159
158
  name: projectId + (p.displayName ? ` (${p.displayName})` : ""),
160
159
  value: projectId,
161
160
  };
162
- });
163
- choices = _.orderBy(choices, ["name"], ["asc"]);
161
+ })
162
+ .sort((a, b) => a.name.localeCompare(b.name));
164
163
  return await (0, prompt_1.promptOnce)({
165
164
  type: "list",
166
165
  name: "id",
@@ -303,14 +303,14 @@ class ProfileReport {
303
303
  });
304
304
  });
305
305
  const paths = Object.keys(unindexed);
306
- paths.forEach((path) => {
306
+ for (const path of paths) {
307
307
  const indices = Object.keys(unindexed[path]);
308
- indices.forEach((index) => {
308
+ for (const index of indices) {
309
309
  const data = unindexed[path][index];
310
310
  const row = [path, extractReadableIndex(data.query), formatNumber(data.times)];
311
311
  table.push(row);
312
- });
313
- });
312
+ }
313
+ }
314
314
  return table;
315
315
  }
316
316
  renderBandwidth(pureData) {
@@ -327,8 +327,10 @@ class ProfileReport {
327
327
  times: b1.times + b2.times,
328
328
  };
329
329
  });
330
- const paths = _.orderBy(Object.keys(data), [(p) => data[p].bytes], ["desc"]);
331
- paths.forEach((path) => {
330
+ const paths = Object.keys(data).sort((a, b) => {
331
+ return data[b].bytes - data[a].bytes;
332
+ });
333
+ for (const path of paths) {
332
334
  const bandwidth = data[path];
333
335
  const row = [
334
336
  path,
@@ -337,7 +339,7 @@ class ProfileReport {
337
339
  formatBytes(bandwidth.bytes / bandwidth.times),
338
340
  ];
339
341
  table.push(row);
340
- });
342
+ }
341
343
  return table;
342
344
  }
343
345
  renderOutgoingBandwidth() {
@@ -392,12 +394,12 @@ class ProfileReport {
392
394
  rejected: s1.rejected + s2.rejected,
393
395
  };
394
396
  });
395
- let paths = Object.keys(data);
396
- paths = _.orderBy(paths, (path) => {
397
- const speed = data[path];
398
- return speed.millis / speed.times;
399
- }, ["desc"]);
400
- paths.forEach((path) => {
397
+ const paths = Object.keys(data).sort((a, b) => {
398
+ const speedA = data[a].millis / data[a].times;
399
+ const speedB = data[b].millis / data[b].times;
400
+ return speedB - speedA;
401
+ });
402
+ for (const path of paths) {
401
403
  const speed = data[path];
402
404
  const row = [
403
405
  path,
@@ -409,7 +411,7 @@ class ProfileReport {
409
411
  row.push(formatNumber(speed.rejected));
410
412
  }
411
413
  table.push(row);
412
- });
414
+ }
413
415
  return table;
414
416
  }
415
417
  renderReadSpeed() {
package/lib/rc.js CHANGED
@@ -25,7 +25,7 @@ exports.loadRC = loadRC;
25
25
  class RC {
26
26
  constructor(rcpath, data) {
27
27
  this.path = rcpath;
28
- this.data = Object.assign({ projects: {}, targets: {} }, data);
28
+ this.data = Object.assign({ projects: {}, targets: {}, etags: {} }, data);
29
29
  }
30
30
  static loadFile(rcpath) {
31
31
  let data = {};
@@ -136,18 +136,22 @@ class RC {
136
136
  requireTarget(project, type, name) {
137
137
  const target = this.target(project, type, name);
138
138
  if (!target.length) {
139
- throw new error_1.FirebaseError("Deploy target " +
140
- clc.bold(name) +
141
- " not configured for project " +
142
- clc.bold(project) +
143
- ". Configure with:\n\n firebase target:apply " +
144
- type +
145
- " " +
146
- name +
147
- " <resources...>");
139
+ throw new error_1.FirebaseError(`Deploy target ${clc.bold(name)} not configured for project ${clc.bold(project)}. Configure with:
140
+
141
+ firebase target:apply ${type} ${name} <resources...>`);
148
142
  }
149
143
  return target;
150
144
  }
145
+ getEtags(projectId) {
146
+ return this.data.etags[projectId] || { extensionInstances: {} };
147
+ }
148
+ setEtags(projectId, resourceType, etagData) {
149
+ if (!this.data.etags[projectId]) {
150
+ this.data.etags[projectId] = {};
151
+ }
152
+ this.data.etags[projectId][resourceType] = etagData;
153
+ this.save();
154
+ }
151
155
  save() {
152
156
  if (this.path) {
153
157
  fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2), {
@@ -3,11 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.requireConfig = void 0;
4
4
  const error_1 = require("./error");
5
5
  async function requireConfig(options) {
6
- await Promise.resolve();
7
- if (!options.config) {
8
- throw options.configError
9
- ? options.configError
10
- : new error_1.FirebaseError("Not in a Firebase project directory (could not locate firebase.json)");
11
- }
6
+ return new Promise((resolve, reject) => {
7
+ var _a;
8
+ return options.config
9
+ ? resolve()
10
+ : reject((_a = options.configError) !== null && _a !== void 0 ? _a : new error_1.FirebaseError("Not in a Firebase project directory (could not locate firebase.json)"));
11
+ });
12
12
  }
13
13
  exports.requireConfig = requireConfig;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RulesDeploy = exports.RulesetServiceType = void 0;
4
4
  const _ = require("lodash");
5
5
  const clc = require("cli-color");
6
- const fs = require("fs");
6
+ const fs = require("fs-extra");
7
7
  const gcp = require("./gcp");
8
8
  const logger_1 = require("./logger");
9
9
  const error_1 = require("./error");
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "11.2.1",
3
+ "version": "11.4.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "firebase-tools",
9
- "version": "11.2.1",
9
+ "version": "11.4.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@google-cloud/pubsub": "^3.0.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "11.2.1",
3
+ "version": "11.4.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  "test:extensions-deploy": "bash ./scripts/extensions-deploy-tests/run.sh",
31
31
  "test:extensions-emulator": "bash ./scripts/extensions-emulator-tests/run.sh",
32
32
  "test:hosting": "bash ./scripts/hosting-tests/run.sh",
33
+ "test:hosting-rewrites": "bash ./scripts/hosting-tests/rewrites-tests/run.sh",
33
34
  "test:triggers-end-to-end": "bash ./scripts/triggers-end-to-end-tests/run.sh",
34
35
  "test:storage-deploy": "bash ./scripts/storage-deploy-tests/run.sh",
35
36
  "test:storage-emulator-integration": "bash ./scripts/storage-emulator-integration/run.sh"