firebase-tools 15.10.1 → 15.12.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 (54) hide show
  1. package/lib/agentSkills.js +70 -0
  2. package/lib/api.js +3 -1
  3. package/lib/appdistribution/client.js +17 -0
  4. package/lib/apphosting/backend.js +22 -3
  5. package/lib/apptesting/parseTestFiles.js +11 -8
  6. package/lib/bin/mcp.js +5 -1
  7. package/lib/commands/apphosting-backends-create.js +19 -2
  8. package/lib/commands/apphosting-backends-list.js +21 -5
  9. package/lib/commands/apptesting.js +16 -7
  10. package/lib/commands/functions-delete.js +1 -0
  11. package/lib/commands/functions-export.js +40 -0
  12. package/lib/commands/index.js +3 -0
  13. package/lib/commands/init.js +1 -0
  14. package/lib/commands/studio-export.js +2 -2
  15. package/lib/deploy/apphosting/deploy.js +11 -6
  16. package/lib/deploy/apphosting/prepare.js +21 -1
  17. package/lib/deploy/apphosting/release.js +2 -5
  18. package/lib/deploy/apphosting/util.js +45 -2
  19. package/lib/deploy/firestore/prepare.js +17 -0
  20. package/lib/deploy/functions/prepare.js +4 -1
  21. package/lib/deploy/functions/release/fabricator.js +4 -3
  22. package/lib/deploy/functions/release/index.js +5 -0
  23. package/lib/deploy/functions/runtimes/dart/index.js +282 -0
  24. package/lib/deploy/functions/runtimes/index.js +1 -1
  25. package/lib/deploy/functions/runtimes/supported/index.js +4 -0
  26. package/lib/deploy/functions/services/ailogic.js +68 -0
  27. package/lib/deploy/functions/services/index.js +4 -0
  28. package/lib/emulator/downloadableEmulatorInfo.json +30 -30
  29. package/lib/emulator/functionsEmulator.js +103 -24
  30. package/lib/emulator/functionsRuntimeWorker.js +21 -18
  31. package/lib/emulator/storage/rules/manager.js +10 -3
  32. package/lib/emulator/storage/rules/runtime.js +9 -7
  33. package/lib/experiments.js +22 -0
  34. package/lib/firebase_studio/migrate.js +83 -70
  35. package/lib/functions/iac/export.js +36 -0
  36. package/lib/functions/iac/terraform.js +146 -0
  37. package/lib/gcp/ailogic.js +108 -0
  38. package/lib/gcp/cloudfunctionsv2.js +24 -0
  39. package/lib/init/features/agentSkills.js +26 -0
  40. package/lib/init/features/dataconnect/sdk.js +26 -12
  41. package/lib/init/features/functions/dart.js +31 -0
  42. package/lib/init/features/functions/index.js +14 -0
  43. package/lib/init/features/index.js +4 -1
  44. package/lib/init/index.js +6 -0
  45. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  46. package/lib/utils.js +8 -0
  47. package/package.json +5 -3
  48. package/schema/firebase-config.json +7 -0
  49. package/standalone/package.json +1 -1
  50. package/templates/firebase-studio-export/readme_template.md +2 -0
  51. package/templates/init/functions/dart/_gitignore +11 -0
  52. package/templates/init/functions/dart/pubspec.yaml +14 -0
  53. package/templates/init/functions/dart/server.dart +15 -0
  54. package/lib/deploy/functions/runtimes/dart.js +0 -42
@@ -35,6 +35,8 @@ const v1_1 = require("../functions/events/v1");
35
35
  const build_1 = require("../deploy/functions/build");
36
36
  const env_1 = require("./env");
37
37
  const python_1 = require("../functions/python");
38
+ const supported_1 = require("../deploy/functions/runtimes/supported");
39
+ const dart_1 = require("../deploy/functions/runtimes/dart");
38
40
  const EVENT_INVOKE_GA4 = "functions_invoke";
39
41
  const DATABASE_PATH_PATTERN = new RegExp("^projects/[^/]+/instances/([^/]+)/refs(/.*)$");
40
42
  class IPCConn {
@@ -83,6 +85,7 @@ class FunctionsEmulator {
83
85
  this.staticBackends = [];
84
86
  this.dynamicBackends = [];
85
87
  this.watchers = [];
88
+ this.watchCleanups = [];
86
89
  this.debugMode = false;
87
90
  this.staticBackends = args.emulatableBackends;
88
91
  emulatorLogger_1.EmulatorLogger.setVerbosity(this.args.verbosity ? emulatorLogger_1.Verbosity[this.args.verbosity] : emulatorLogger_1.Verbosity["DEBUG"]);
@@ -208,7 +211,7 @@ class FunctionsEmulator {
208
211
  async sendRequest(trigger, body) {
209
212
  const record = this.getTriggerRecordByKey(this.getTriggerKey(trigger));
210
213
  const pool = this.workerPools[record.backend.codebase];
211
- if (!pool.readyForWork(trigger.id)) {
214
+ if (!pool.readyForWork(trigger.id, record.backend.runtime)) {
212
215
  try {
213
216
  await this.startRuntime(record.backend, trigger);
214
217
  }
@@ -217,7 +220,7 @@ class FunctionsEmulator {
217
220
  return;
218
221
  }
219
222
  }
220
- const worker = pool.getIdleWorker(trigger.id);
223
+ const worker = pool.getIdleWorker(trigger.id, record.backend.runtime);
221
224
  if (this.debugMode) {
222
225
  await worker.sendDebugMsg({
223
226
  functionTarget: trigger.entryPoint,
@@ -229,10 +232,13 @@ class FunctionsEmulator {
229
232
  "Content-Type": "application/json",
230
233
  "Content-Length": `${reqBody.length}`,
231
234
  };
235
+ const isDart = (0, supported_1.isLanguageRuntime)(record.backend.runtime, "dart");
236
+ const path = isDart ? `/${trigger.entryPoint}` : `/`;
232
237
  return new Promise((resolve, reject) => {
233
238
  const req = http.request({
234
239
  ...worker.runtime.conn.httpReqOpts(),
235
- path: `/`,
240
+ method: "POST",
241
+ path: path,
236
242
  headers: headers,
237
243
  }, resolve);
238
244
  req.on("error", reject);
@@ -264,23 +270,43 @@ class FunctionsEmulator {
264
270
  async connect() {
265
271
  for (const backend of this.staticBackends) {
266
272
  this.logger.logLabeled("BULLET", "functions", `Watching "${backend.functionsDir}" for Cloud Functions...`);
267
- const watcher = chokidar.watch(backend.functionsDir, {
268
- ignored: [
269
- /.+?[\\\/]node_modules[\\\/].+?/,
270
- /(^|[\/\\])\../,
271
- /.+\.log/,
272
- /.+?[\\\/]venv[\\\/].+?/,
273
- ...(backend.ignore?.map((i) => `**/${i}`) ?? []),
274
- ],
275
- persistent: true,
276
- });
277
- this.watchers.push(watcher);
278
- const debouncedLoadTriggers = (0, utils_1.debounce)(() => this.loadTriggers(backend), 1000);
279
- watcher.on("change", (filePath) => {
280
- this.logger.log("DEBUG", `File ${filePath} changed, reloading triggers`);
281
- return debouncedLoadTriggers();
282
- });
283
273
  await this.loadTriggers(backend, true);
274
+ const isDart = (0, supported_1.isLanguageRuntime)(backend.runtime, "dart");
275
+ if (isDart) {
276
+ const runtimeDelegateContext = {
277
+ projectId: this.args.projectId,
278
+ projectDir: this.args.projectDir,
279
+ sourceDir: backend.functionsDir,
280
+ runtime: backend.runtime,
281
+ };
282
+ const delegate = await runtimes.getRuntimeDelegate(runtimeDelegateContext);
283
+ this.logger.logLabeled("BULLET", "functions", `Starting build_runner watch for Dart functions...`);
284
+ const debouncedLoadTriggers = (0, utils_1.debounce)(() => this.loadTriggers(backend), 1000);
285
+ const cleanup = await delegate.watch(() => {
286
+ this.logger.log("DEBUG", "build_runner rebuilt, reloading triggers");
287
+ debouncedLoadTriggers();
288
+ });
289
+ this.watchCleanups.push(cleanup);
290
+ this.logger.logLabeled("SUCCESS", "functions", `build_runner initial build completed`);
291
+ }
292
+ else {
293
+ const watcher = chokidar.watch(backend.functionsDir, {
294
+ ignored: [
295
+ /(^|[\/\\])\../,
296
+ /.+\.log/,
297
+ /.+?[\\\/]node_modules[\\\/].+?/,
298
+ /.+?[\\\/]venv[\\\/].+?/,
299
+ ...(backend.ignore?.map((i) => `**/${i}`) ?? []),
300
+ ],
301
+ persistent: true,
302
+ });
303
+ this.watchers.push(watcher);
304
+ const debouncedLoadTriggers = (0, utils_1.debounce)(() => this.loadTriggers(backend), 1000);
305
+ watcher.on("change", (filePath) => {
306
+ this.logger.log("DEBUG", `File ${filePath} changed, reloading triggers`);
307
+ return debouncedLoadTriggers();
308
+ });
309
+ }
284
310
  }
285
311
  await this.performPostLoadOperations();
286
312
  return;
@@ -300,6 +326,10 @@ class FunctionsEmulator {
300
326
  await watcher.close();
301
327
  }
302
328
  this.watchers = [];
329
+ for (const cleanup of this.watchCleanups) {
330
+ await cleanup();
331
+ }
332
+ this.watchCleanups = [];
303
333
  if (this.destroyServer) {
304
334
  await this.destroyServer();
305
335
  }
@@ -1098,13 +1128,52 @@ class FunctionsEmulator {
1098
1128
  conn: new TCPConn("127.0.0.1", port),
1099
1129
  };
1100
1130
  }
1131
+ async startDart(backend, envs) {
1132
+ if (this.debugMode) {
1133
+ this.logger.log("WARN", "--inspect-functions not supported for Dart functions. Ignored.");
1134
+ }
1135
+ const port = await portfinder.getPortPromise({
1136
+ port: 8081 + (0, utils_1.randomInt)(0, 1000),
1137
+ });
1138
+ const args = ["run", "--no-serve-devtools", dart_1.DART_ENTRY_POINT];
1139
+ const dartEnvs = { ...envs };
1140
+ delete dartEnvs.FUNCTION_TARGET;
1141
+ delete dartEnvs.FUNCTION_SIGNATURE_TYPE;
1142
+ const bin = backend.bin || "dart";
1143
+ logger_1.logger.debug(`Starting Dart runtime with args: ${args.join(" ")} on port ${port}`);
1144
+ const childProcess = spawn(bin, args, {
1145
+ cwd: backend.functionsDir,
1146
+ env: {
1147
+ ...process.env,
1148
+ ...dartEnvs,
1149
+ HOST: "127.0.0.1",
1150
+ PORT: port.toString(),
1151
+ },
1152
+ stdio: ["pipe", "pipe", "pipe"],
1153
+ });
1154
+ childProcess.stdout?.on("data", (chunk) => {
1155
+ this.logger.log("DEBUG", `[dart] ${chunk.toString("utf8")}`);
1156
+ });
1157
+ childProcess.stderr?.on("data", (chunk) => {
1158
+ this.logger.log("DEBUG", `[dart] ${chunk.toString("utf8")}`);
1159
+ });
1160
+ return {
1161
+ process: childProcess,
1162
+ events: new events_1.EventEmitter(),
1163
+ cwd: backend.functionsDir,
1164
+ conn: new TCPConn("127.0.0.1", port),
1165
+ };
1166
+ }
1101
1167
  async startRuntime(backend, trigger) {
1102
1168
  const runtimeEnv = this.getRuntimeEnvs(backend, trigger);
1103
1169
  const secretEnvs = await this.resolveSecretEnvs(backend, trigger);
1104
1170
  let runtime;
1105
- if (backend.runtime.startsWith("python")) {
1171
+ if ((0, supported_1.isLanguageRuntime)(backend.runtime, "python")) {
1106
1172
  runtime = await this.startPython(backend, { ...runtimeEnv, ...secretEnvs });
1107
1173
  }
1174
+ else if ((0, supported_1.isLanguageRuntime)(backend.runtime, "dart")) {
1175
+ runtime = await this.startDart(backend, { ...runtimeEnv, ...secretEnvs });
1176
+ }
1108
1177
  else {
1109
1178
  runtime = await this.startNode(backend, { ...runtimeEnv, ...secretEnvs });
1110
1179
  }
@@ -1113,7 +1182,7 @@ class FunctionsEmulator {
1113
1182
  ref: backend.extensionVersion?.ref,
1114
1183
  };
1115
1184
  const pool = this.workerPools[backend.codebase];
1116
- const worker = pool.addWorker(trigger, runtime, extensionLogInfo);
1185
+ const worker = pool.addWorker(trigger, runtime, extensionLogInfo, backend.runtime);
1117
1186
  await worker.waitForSocketReady();
1118
1187
  return worker;
1119
1188
  }
@@ -1172,10 +1241,20 @@ class FunctionsEmulator {
1172
1241
  });
1173
1242
  this.logger.log("DEBUG", `[functions] Runtime ready! Sending request!`);
1174
1243
  const url = new url_1.URL(`${req.protocol}://${req.hostname}${req.url}`);
1175
- const path = `${url.pathname}${url.search}`.replace(new RegExp(`\/${this.args.projectId}\/[^\/]*\/${req.params.trigger_name}\/?`), "/");
1244
+ let path = `${url.pathname}${url.search}`.replace(new RegExp(`\/${this.args.projectId}\/[^\/]*\/${req.params.trigger_name}\/?`), "/");
1245
+ const isDart = (0, supported_1.isLanguageRuntime)(record.backend.runtime, "dart");
1246
+ if (isDart) {
1247
+ const isBackgroundRoute = req.url.startsWith("/functions/projects/");
1248
+ if (isBackgroundRoute || path === "/") {
1249
+ path = `/${trigger.entryPoint}`;
1250
+ }
1251
+ else {
1252
+ path = `/${trigger.entryPoint}${path}`;
1253
+ }
1254
+ }
1176
1255
  this.logger.log("DEBUG", `[functions] Got req.url=${req.url}, mapping to path=${path}`);
1177
1256
  const pool = this.workerPools[record.backend.codebase];
1178
- if (!pool.readyForWork(trigger.id)) {
1257
+ if (!pool.readyForWork(trigger.id, record.backend.runtime)) {
1179
1258
  try {
1180
1259
  await this.startRuntime(record.backend, trigger);
1181
1260
  }
@@ -1196,7 +1275,7 @@ class FunctionsEmulator {
1196
1275
  method,
1197
1276
  path,
1198
1277
  headers: req.headers,
1199
- }, res, reqBody, debugBundle);
1278
+ }, res, reqBody, debugBundle, record.backend.runtime);
1200
1279
  }
1201
1280
  }
1202
1281
  exports.FunctionsEmulator = FunctionsEmulator;
@@ -8,6 +8,7 @@ const events_1 = require("events");
8
8
  const emulatorLogger_1 = require("./emulatorLogger");
9
9
  const error_1 = require("../error");
10
10
  const discovery_1 = require("../deploy/functions/runtimes/discovery");
11
+ const supported_1 = require("../deploy/functions/runtimes/supported");
11
12
  var RuntimeWorkerState;
12
13
  (function (RuntimeWorkerState) {
13
14
  RuntimeWorkerState["CREATED"] = "CREATED";
@@ -226,13 +227,14 @@ class RuntimeWorkerPool {
226
227
  this.mode = mode;
227
228
  this.workers = new Map();
228
229
  }
229
- getKey(triggerId) {
230
+ getKey(triggerId, runtime) {
230
231
  if (this.mode === types_1.FunctionsExecutionMode.SEQUENTIAL) {
231
232
  return "~shared~";
232
233
  }
233
- else {
234
- return triggerId || "~diagnostic~";
234
+ if ((0, supported_1.isLanguageRuntime)(runtime, "dart")) {
235
+ return "~dart-shared~";
235
236
  }
237
+ return triggerId || "~diagnostic~";
236
238
  }
237
239
  refresh() {
238
240
  for (const arr of this.workers.values()) {
@@ -261,13 +263,13 @@ class RuntimeWorkerPool {
261
263
  });
262
264
  }
263
265
  }
264
- readyForWork(triggerId) {
265
- const idleWorker = this.getIdleWorker(triggerId);
266
+ readyForWork(triggerId, runtime) {
267
+ const idleWorker = this.getIdleWorker(triggerId, runtime);
266
268
  return !!idleWorker;
267
269
  }
268
- async submitRequest(triggerId, req, resp, body, debug) {
270
+ async submitRequest(triggerId, req, resp, body, debug, runtime) {
269
271
  this.log(`submitRequest(triggerId=${triggerId})`);
270
- const worker = this.getIdleWorker(triggerId);
272
+ const worker = this.getIdleWorker(triggerId, runtime);
271
273
  if (!worker) {
272
274
  throw new error_1.FirebaseError("Internal Error: can't call submitRequest without checking for idle workers");
273
275
  }
@@ -276,11 +278,11 @@ class RuntimeWorkerPool {
276
278
  }
277
279
  return worker.request(req, resp, body, !!debug);
278
280
  }
279
- getIdleWorker(triggerId) {
281
+ getIdleWorker(triggerId, runtime) {
280
282
  this.cleanUpWorkers();
281
- const triggerWorkers = this.getTriggerWorkers(triggerId);
283
+ const triggerWorkers = this.getTriggerWorkers(triggerId, runtime);
282
284
  if (!triggerWorkers.length) {
283
- this.setTriggerWorkers(triggerId, []);
285
+ this.setTriggerWorkers(triggerId, [], runtime);
284
286
  return;
285
287
  }
286
288
  for (const worker of triggerWorkers) {
@@ -290,21 +292,22 @@ class RuntimeWorkerPool {
290
292
  }
291
293
  return;
292
294
  }
293
- addWorker(trigger, runtime, extensionLogInfo) {
294
- this.log(`addWorker(${this.getKey(trigger?.id)})`);
295
+ addWorker(trigger, runtime, extensionLogInfo, runtimeType) {
296
+ const key = this.getKey(trigger?.id, runtimeType);
297
+ this.log(`addWorker(${key})`);
295
298
  const disableTimeout = !trigger?.id || this.mode === types_1.FunctionsExecutionMode.SEQUENTIAL;
296
299
  const worker = new RuntimeWorker(trigger?.id, runtime, extensionLogInfo, disableTimeout ? undefined : trigger?.timeoutSeconds);
297
- const keyWorkers = this.getTriggerWorkers(trigger?.id);
300
+ const keyWorkers = this.getTriggerWorkers(trigger?.id, runtimeType);
298
301
  keyWorkers.push(worker);
299
- this.setTriggerWorkers(trigger?.id, keyWorkers);
302
+ this.setTriggerWorkers(trigger?.id, keyWorkers, runtimeType);
300
303
  this.log(`Adding worker with key ${worker.triggerKey}, total=${keyWorkers.length}`);
301
304
  return worker;
302
305
  }
303
- getTriggerWorkers(triggerId) {
304
- return this.workers.get(this.getKey(triggerId)) || [];
306
+ getTriggerWorkers(triggerId, runtime) {
307
+ return this.workers.get(this.getKey(triggerId, runtime)) || [];
305
308
  }
306
- setTriggerWorkers(triggerId, workers) {
307
- this.workers.set(this.getKey(triggerId), workers);
309
+ setTriggerWorkers(triggerId, workers, runtime) {
310
+ this.workers.set(this.getKey(triggerId, runtime), workers);
308
311
  }
309
312
  cleanUpWorkers() {
310
313
  for (const [key, keyWorkers] of this.workers.entries()) {
@@ -34,9 +34,16 @@ class DefaultStorageRulesManager {
34
34
  .watch(rulesFile, { persistent: true, ignoreInitial: true })
35
35
  .on("change", async () => {
36
36
  await new Promise((res) => setTimeout(res, 5));
37
- this._logger.logLabeled("BULLET", "storage", "Change detected, updating rules for Cloud Storage...");
38
- this._rules.content = (0, fsutils_1.readFile)(rulesFile);
39
- await this.loadRuleset();
37
+ try {
38
+ const content = (0, fsutils_1.readFile)(rulesFile);
39
+ this._rules.content = content;
40
+ this._logger.logLabeled("BULLET", "storage", "Change detected, updating rules for Cloud Storage...");
41
+ await this.loadRuleset();
42
+ }
43
+ catch (e) {
44
+ const message = e instanceof Error ? e.message : String(e);
45
+ this._logger.logLabeled("DEBUG", "storage", `A rule file change was detected, but there was an error reading it: ${message}`);
46
+ }
40
47
  });
41
48
  }
42
49
  async loadRuleset() {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StorageRulesRuntime = exports.StorageRulesIssues = exports.StorageRulesetInstance = void 0;
4
+ exports.createAuthExpressionValue = createAuthExpressionValue;
4
5
  const cross_spawn_1 = require("cross-spawn");
5
6
  const error_1 = require("../../../error");
6
7
  const AsyncLock = require("async-lock");
@@ -332,14 +333,15 @@ function createAuthExpressionValue(opts) {
332
333
  if (!opts.token) {
333
334
  return toExpressionValue(null);
334
335
  }
335
- else {
336
- const tokenPayload = jwt.decode(opts.token, { json: true });
337
- const jsonValue = {
338
- uid: tokenPayload.user_id,
339
- token: tokenPayload,
340
- };
341
- return toExpressionValue(jsonValue);
336
+ const tokenPayload = jwt.decode(opts.token, { json: true });
337
+ if (typeof tokenPayload !== "object" || !tokenPayload) {
338
+ return toExpressionValue(null);
342
339
  }
340
+ const jsonValue = {
341
+ uid: "user_id" in tokenPayload ? tokenPayload.user_id : undefined,
342
+ token: tokenPayload,
343
+ };
344
+ return toExpressionValue(jsonValue);
343
345
  }
344
346
  function createRequestExpressionValue(opts) {
345
347
  const fields = {
@@ -58,10 +58,22 @@ exports.ALL_EXPERIMENTS = experiments({
58
58
  shortDescription: "Functions created using the V2 API target Cloud Run Functions (not production ready)",
59
59
  public: false,
60
60
  },
61
+ functionsiac: {
62
+ shortDescription: "Exports functions IaC code",
63
+ public: false,
64
+ },
61
65
  functionsrunapionly: {
62
66
  shortDescription: "Use Cloud Run API to list v2 functions",
63
67
  public: false,
64
68
  },
69
+ bypassfunctionsdeprecationcheck: {
70
+ shortDescription: "Bypass Functions check for old runtimes",
71
+ fullDescription: "Bypasses the local check for whether a functions runtime is " +
72
+ "decommissioned. This does not, by itself, allow you to deploy a function with a " +
73
+ "decommissioned runtime, as there are server-side checks as well.",
74
+ public: false,
75
+ default: false,
76
+ },
65
77
  emulatoruisnapshot: {
66
78
  shortDescription: "Load pre-release versions of the emulator UI",
67
79
  },
@@ -109,6 +121,11 @@ exports.ALL_EXPERIMENTS = experiments({
109
121
  default: false,
110
122
  public: false,
111
123
  },
124
+ abiu: {
125
+ shortDescription: "Enable App Hosting ABIU and runtime selection",
126
+ default: false,
127
+ public: false,
128
+ },
112
129
  dataconnect: {
113
130
  shortDescription: "Deprecated. Previosuly, enabled Data Connect related features.",
114
131
  fullDescription: "Deprecated. Previously, enabled Data Connect related features.",
@@ -150,6 +167,11 @@ exports.ALL_EXPERIMENTS = experiments({
150
167
  default: true,
151
168
  public: false,
152
169
  },
170
+ fdcrealtime: {
171
+ shortDescription: "Enable Firebase Data Connect realtime feature.",
172
+ default: false,
173
+ public: false,
174
+ },
153
175
  });
154
176
  function isValidExperiment(name) {
155
177
  return Object.keys(exports.ALL_EXPERIMENTS).includes(name);