将 Firebase Extensions 迁移到 Cloud Functions

本指南介绍了如何将扩展程序从已弃用的 Firebase Extensions 环境迁移到用户在其自己的 Cloud Functions for Firebase(第 2 代)代码库中安装和部署的函数。

这是推荐的迁移路径。Firebase 将维护一个包含官方 npm 等效项的扩展程序列表;本指南将引导您创建自己的扩展程序。

在本指南中,Stream Cloud Firestore to BigQuery 扩展程序 (firestore-bigquery-export) 将用作一个贯穿始终的示例。每个部分都以实际示例结尾,展示了该扩展程序在迁移前后的样子,迁移后为 @firebase-function-kits/firestore-bigquery-export 软件包。

注册即可获取有关迁移扩展程序的更多信息和帮助

如果您对如何从 Firebase Extensions 迁移有疑问,可以通过 firebase-extensions-migrator-support-external@google.com 与我们联系。我们还会在更新指南时向此群组发送电子邮件,提供有关打包、测试和分发第 2 代函数的更多信息。

如需加入此群组,请向 firebase-extensions-migrator-support-external+subscribe@google.com 发送邮件,您将收到一封成员资格请求邮件。您必须回复该电子邮件,而不是点击“加入此群组”按钮。

准备工作

如需按照说明完成此迁移,您需要使用 Cloud Functions 的以下功能:

  • 参数化配置。 您在 extension.yaml 中声明的每个形参都会成为软件包代码中定义的形参。

  • 声明式 IAM 角色和必需的 API。您在 extension.yaml 中声明的每个角色都会成为 requiresRole(...) 调用,每个 API 都会成为软件包代码中的 requiresAPI(...) 调用。在部署时,Firebase CLI 会代表您向代管式运行时服务账号授予声明的角色,并启用声明的 API。

  • Cloud Functions 代码库的生命周期事件。Cloud Functions 代码库现在支持类似于 Firebase Extensions 的生命周期事件。使用生命周期钩子 afterFirstDeploy(...) 和 afterRedeploy(...) 声明安装时和更新时设置。这些会替换您在 extension.yaml 中声明的 lifecycleEvents。

将 Firebase Extensions 源代码迁移到第 2 代函数

(可选)使用 Firebase 智能体技能自动迁移

您可以使用官方 extension-to-functions-codebase AI 智能体技能自动执行第 1 步到第 8 步(盘点资源、触发升级、参数和 Secret 转换、声明式 IAM、生命周期钩子以及生成软件包 README)。

安装技能

如果您或您的 AI 编码助理(Gemini in Firebase、Cursor、Claude Code、GitHub Copilot)尚未安装该技能,请使用技能 CLI 运行以下命令:

npx skills add firebase/agent-skills --skill extension-to-functions-codebase

在项目中安装技能后,AI 编码助理会自动遵循其迁移规则和转换步骤。您可以使用以下提示:

“请按照 extension-to-functions-codebase skill 中的说明,将此 Firebase Extensions 迁移到可发布的第 2 代 Functions 软件包中。”

1. 盘点扩展程序

首先,对扩展程序进行盘点:列出扩展程序声明、交付和记录的所有内容的完整清单,以便每个行为在第 2 代函数中都有一个明确的目标,并且在迁移过程中不会丢失任何内容。

查看以下各项,并记下您的发现:

  • extension.yaml,用于声明参数、函数、事件、IAM 角色、必需的 API、密钥和生命周期钩子。

  • functions/,其中包含函数代码、依赖项、build 配置、触发器和任务队列函数。

  • README.md、PREINSTALL.md 和 POSTINSTALL.md,其中包含设置步骤、警告和结算说明。

  • scripts/,其中包含任何导入、回填、IAM、修复或迁移实用程序,以及您随扩展程序一起提供的任何其他工具。

然后,对于 extension.yaml 中的每个项,确定其在 npm 软件包中的位置:

  • 将用户配置转换为 Cloud Functions 参数(第 4 步)。

  • 将 Secret 转换为 Cloud Functions Secret(第 4 步)。

  • 将 IAM 角色转换为 requiresRole(...) 声明(第 6 步)。

  • 在适当情况下,将所需的 Google API 转换为 requiresAPI(...) 声明(第 6 步)。

  • 将安装和更新钩子转换为 afterFirstDeploy(...) 和 afterRedeploy(...) 声明(第 7 步)。

  • 将实例 ID 从 EXT_INSTANCE_ID 转换为 FIREBASE_KIT_INSTANCE_ID(第 4 步)。

实际示例:从 Cloud Firestore 流式传输到 BigQuery

读取 firestore-bigquery-export/extension.yaml 和 functions/ 会生成以下清单:

在extension.yaml 数量/价值 去向
params 25(COLLECTION_PATH、DATASET_ID、TABLE_ID、DATASET_LOCATION、VIEW_TYPE…) Cloud Functions 参数(第 4 步)
apis bigquery.googleapis.com requiresAPI(...)(第 6 步)
roles bigquery.dataEditor、datastore.user、bigquery.user requiresRole(...)(第 6 步)
resources 1 个事件触发器 (fsexportbigquery) + 任务队列函数(initBigQuerySync、setupBigQuerySync) 导出的软件包函数(第 3 步)
lifecycleEvents onInstall → initBigQuerySync;onUpdate/onConfigure → setupBigQuerySync afterFirstDeploy/afterRedeploy(第 7 步)
实例 ID 未使用(无 EXT_INSTANCE_ID 读取) 没有可迁移的内容
scripts/ import/(补余)、gen-schema-view/ 保留为脚本(此处不作讨论)

分析。该扩展程序未声明任何 type: secret 参数,因此在第 4 步中无需迁移任何密文。事件触发器已是第 2 代;只有任务队列函数仍是第 1 代(与第 3 步相关)。

2. 更新 package.json

更新扩展程序的 package.json 文件。如果您要迁移一个扩展程序,则可以是根 package.json。如果您要在一个代码库中迁移多个扩展程序,请为每个扩展程序提供自己的软件包。

最低 SDK 版本:将 firebase-functions >= 7.4.0 和 firebase-admin >= 14.2.0 声明为依赖项。同时将您的 firebase-functions 版本声明为对等依赖项,以便用户的 Cloud Functions 项目具有与您的库编写时所用的 SDK 相同的版本。

{
  "name": "<package-name>",
  "version": "1.0.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "exports": {
    ".": {
      "types": "./lib/index.d.ts",
      "default": "./lib/index.js"
    }
  },
  "engines": {
    "node": "22"
  },
  "peerDependencies": {
    "firebase-functions": "^7.4.0"
  },
  "dependencies": {
    "firebase-functions": "^7.4.0",
    "firebase-admin": "^14.2.0"
  }
}

实际示例:从 Cloud Firestore 流式传输到 BigQuery

之前。扩展程序的 functions/package.json 是私有的,用于命名扩展程序 ID,并将 firebase-functions 声明为直接依赖项:

{
  "name": "firestore-bigquery-export",
  "main": "lib/index.js",
  "private": true,
  "dependencies": {
    "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
    "firebase-admin": "^14.2.0",
    "firebase-functions": "^6.3.2"
  }
}

之后。可发布的软件包:限定范围的名称、exports 映射,以及移至 peerDependencies 的 firebase-functions:

{
  "name": "@firebase-function-kits/firestore-bigquery-export",
  "version": "0.1.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "exports": {
    ".": {
      "types": "./lib/index.d.ts",
      "default": "./lib/index.js"
    }
  },
  "engines": {
    "node": "22"
  },
  "peerDependencies": {
    "firebase-functions": "^7.4.0"
  },
  "dependencies": {
    "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
    "firebase-admin": "^14.2.0",
    "firebase-functions": "^7.4.0"
  }
}

3. 将函数从第 1 代升级到第 2 代

如果您的扩展程序仍会导出第 1 代函数,请将每个触发器转换为其第 2 代等效触发器。从 firebase-functions/... 模块导入,并在触发器选项中传递运行时设置。

请参阅 Cloud Functions 第 2 代升级指南。 值得注意的是,您可以使用第 2 代修补事件解构来最大限度地减少重写工作,并避免重写函数逻辑,因为第 2 代 SDK 会将 v1 参数公开为事件对象中的字段,从而允许您使用解构/命名参数并保持业务逻辑不变。

之前。第 1 代:

import * as functions from "firebase-functions/v1";

export const sync = functions.firestore
  .document("{collectionId}/{documentId}")
  .onWrite(async (change, context) => {
    await handleWrite(change.before, change.after, context.params);
  });

之后。第 2 代:

import { onDocumentWritten } from "firebase-functions/firestore";

export const syncV2 = onDocumentWritten(
  { document: "{collectionId}/{documentId}" },
  async ({ change, context }) =>
    await handleWrite(change.before, change.after, context.params)
);

如需查看第 1 代和第 2 代函数之间的差异的完整列表,请参阅 Cloud Functions 版本比较。

第 1 代和第 2 代函数之间的一个显著区别是,第 2 代函数必须与其触发器资源位于同一位置。当用户从使用第 1 代函数的扩展程序迁移到使用第 2 代函数的套件时,可能需要更改函数位置以满足此要求。

由于现有位置会保留,因此用户选择的所有 Cloud Functions 位置在迁移期间都可能会中断。如果扩展程序的事件触发函数依赖于用户提供的函数位置,我们建议向扩展程序添加一个新参数,以收集事件触发器位置并将其映射到新的函数位置。

实际示例:从 Cloud Firestore 流式传输到 BigQuery

defineString("DATABASE_REGION", {
  label: "Firestore Instance Location",
  description:
    "Where is the Firestore database located? You can check your current database location at https://console.cloud.google.com/firestore/databases. The functions in this kit deploy to the Cloud Run region closest to this location.",
  input: select({
    "Multi-region (Europe - Belgium and Netherlands)": "eur3",
    "Multi-region (United States)": "nam5",
    "Multi-region (Iowa, North Virginia, and Oklahoma)": "nam7",
    "Iowa (us-central1)": "us-central1",
    // More locations...
  })
});

// Firestore multi-region locations are not Cloud Run regions; deploying a
// function to one hard-fails, so they map to a region inside the multi-region.
const MULTI_REGION_TO_FUNCTION_REGION: Record<string, string> = {
  nam5: "us-central1",
  nam7: "us-central1",
  eur3: "europe-west1",
};

/**
 * Maps a Firestore database location to the Cloud Run region the functions
 * should deploy to. The lookup is case-insensitive and ignores surrounding
 * whitespace, as the CLI's own region handling is. Regional locations pass
 * through lowercased; an unset or blank location returns `undefined`, meaning
 * the functions declare no region.
 */
export function firestoreLocationToFunctionRegion(
  location: string | undefined
): string | undefined {
  const normalized = location?.trim().toLowerCase();
  if (!normalized) {
    return undefined;
  }
  return MULTI_REGION_TO_FUNCTION_REGION[normalized] ?? normalized;
}

const functionRegion = firestoreLocationToFunctionRegion(
  process.env.DATABASE_REGION
);

export const fsexportbigquery = onDocumentWritten(
  {
    region: functionRegion,
    // Other configuration
  },
  (event) => handleDocumentWrite(event, getHandlerContext())
);

首次部署该套件时,系统会提示用户输入其 DATABASE_REGION,并且函数将部署到上述相应的 functionRegion,从而解决其第 2 代事件触发函数的任何位置问题。

4. 转换扩展程序参数和 Secret

Params

您在 extension.yaml 中声明的每个参数都会成为 Cloud Functions 参数。

转换直接环境读取:

const collectionPath = process.env.COLLECTION_PATH;

转换为 Cloud Functions 参数:

import { defineString } from "firebase-functions/params";
import { onDocumentWritten } from "firebase-functions/firestore";

const collectionPath = defineString("COLLECTION_PATH");

// Pass the param directly when used as a placeholder (e.g. trigger path)
export const sync = onDocumentWritten(
  { document: collectionPath },
  async (event) => {
    // Call .value() to read the string inside a handler
    const path = collectionPath.value();
    await handleWrite(path, event);
  }
);

使用 collectionPath.value() 读取处理程序中的字符串;在需要占位符的位置(例如函数触发器路径)直接使用 collectionPath。

Firebase CLI 会发现您的参数并从 .env、.env.<projectId> 中读取其值,或在部署期间提示用户。请保留相同的参数名称,以便沿用现有安装中的值。

请务必不要更改代码中声明的参数名称。扩展程序迁移会自动保留现有的最终用户参数值,但前提是名称保持不变。

实际示例:从 Cloud Firestore 流式传输到 BigQuery

之前。在 extension.yaml 中声明的参数,在 config.ts 中作为原始环境变量读取:

# extension.yaml
- param: COLLECTION_PATH
  label: Collection path
  type: string
  required: true
// functions/src/config.ts
collectionPath: process.env.COLLECTION_PATH,

之后。一个 defineString;CLI 会发现它并从 .env 中读取:

// src/config.ts
import { defineString } from "firebase-functions/params";

collectionPath: defineString("COLLECTION_PATH", {
  label: "Collection path",
  // We now support "nonEmpty: true" to ensure a value other than the empty string
  // is entered, analogous to "required: true" in extension.yaml
  input: { text: { nonEmpty: true } }
}),

参数名称保持不变,因此现有的 .env 仍可正常运行。

实例 ID

扩展程序会从 EXT_INSTANCE_ID(由扩展程序运行时注入)读取其实例 ID。函数 Kit 会从 FIREBASE_KIT_INSTANCE_ID 中读取其实例 ID,Firebase CLI 会将每个 Kit 实例的 ID 设置为 firebase.json 中 instances 映射中的实例键。CLI 会在部署时发现、模拟器中以及已部署的函数中提供该值。

实例 ID 不是形参,因此请勿使用 defineString 进行声明。事实上,FIREBASE_... 是 .env 文件中的预留前缀,因此用户无法在其中设置或替换该前缀。CLI 注入的值对参数系统不可见。直接从环境中读取:

// Before
const instanceId = process.env.EXT_INSTANCE_ID;

// After
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;

仅当您的软件包作为套件部署时,系统才会设置该变量。如果您的代码也作为独立代码库进行部署(请参阅第 9 步),请将其视为可选代码,或者在缺少该代码时快速失败并显示清晰的消息。如果您的扩展程序将实例 ID 公开为面向用户的参数,则您必须移除该参数,因为 Firebase CLI 现在拥有该值。

实际示例:删除用户数据

(“Stream Cloud Firestore to BigQuery”扩展程序不会读取其实例 ID,因此无需迁移任何内容。“删除用户数据”扩展程序会使用它来命名其 Pub/Sub 主题。)

之前。在 config.ts 中作为原始环境变量读取,并带有 Extensions 用于其资源的前缀 ext-:

// functions/src/config.ts
discoveryTopic: `ext-${process.env.EXT_INSTANCE_ID}-discovery`,
deletionTopic: `ext-${process.env.EXT_INSTANCE_ID}-deletion`,

之后。对用于两个普通参数的 FIREBASE_KIT_INSTANCE_ID 进行普通 process.env 读取,以便用户可以替换主题名称:

// src/config.ts
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;

// Non-empty defaults so Pub/Sub trigger bindings resolve during deploy
// discovery without freezing an empty topic name into the manifest.
discoveryTopicName: defineString("DISCOVERY_TOPIC_NAME", {
  default: `kit-${instanceId}-discovery`,
}),
deletionTopicName: defineString("DELETION_TOPIC_NAME", {
  default: `kit-${instanceId}-deletion`,
}),

默认值必须为非空,因为触发器绑定会在发现时解析。系统会将空默认值作为主题名称写入部署清单。该套件还可防止在套件上下文之外运行。如果缺少该变量,模块级默认值将求值为 kit-undefined-discovery,因此配置加载器会失败,并显示说明性错误:

// ...
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
if (!instanceId) {
  throw new Error(
    "FIREBASE_KIT_INSTANCE_ID is not set. It is provided automatically to " +
      "kit instances by firebase-tools >= 15.32.0; deploy or emulate this " +
      "kit with a supported CLI version."
  );
}
// ...

此检查会在处理程序首次解析其配置时运行,因此缺少变量会产生明确的运行时错误,而不是函数静默绑定到 kit-undefined-* 主题。如需拒绝部署本身,请在模块范围内执行检查,以便在发现期间运行。由于 CLI 从 firebase.json 派生实例 ID,因此没有可配置的 INSTANCE_ID,也无需在多个实例之间保持同步。

密钥

在 extension.yaml 中,您可以使用 type: secret 声明 Secret。Extensions 运行时会存储并绑定这些值,以便您的扩展程序代码可以直接读取 process.env.PARAM_NAME。在典型的 Cloud Functions 代码库中,您需要明确声明并绑定每个 Secret:

import { defineSecret } from "firebase-functions/params";
import { onRequest } from "firebase-functions/https";

const apiKey = defineSecret("API_KEY");
export const fn = onRequest({ secrets: [apiKey] }, handler);

将扩展程序迁移到 npm 软件包/套件后,密钥引用将在最终用户的 .env 文件中进行管理。请务必不要更改代码中声明的 Secret 名称。在迁移期间,最终用户密钥会相应地迁移。

实际示例:从 Cloud Firestore 触发电子邮件

之前。MAIL_COLLECTION 和 SMTP_PASSWORD 在 config.ts 中作为原始环境变量读取:

# extension.yaml
- param: MAIL_COLLECTION
  label: Email documents collection
  type: string
  default: mail
  required: true

- param: SMTP_PASSWORD
  label: SMTP password
  type: secret
// functions/src/config.ts
mailCollection: process.env.MAIL_COLLECTION,
smtpPassword: process.env.SMTP_PASSWORD,

之后。一个 defineString 和一个 defineSecret;CLI 会发现这两个文件并从 .env 中读取:

import { defineString, defineSecret } from "firebase-functions/params";
import { onDocumentWritten } from "firebase-functions/firestore";

const mailCollection = defineString("MAIL_COLLECTION", {
  label: "Email documents collection",
  default: "mail"
});

const smtpPassword = defineSecret("SMTP_PASSWORD", { label: "SMTP password" });

export const processQueue = onDocumentWritten(
  { document: `${mailCollection}/{documentId}`, secrets: [smtpPassword] },
  async (event) => {
    const collection = mailCollection.value();
    const password = smtpPassword.value();
    // ...
  }
);

5. 迁移内部任务队列调用

某些扩展程序会使用 Firebase Admin SDK 从其函数代码内部将工作加入其自己的任务队列。这与接收已分派的任务不同(升级函数和转换生命周期钩子部分对此进行了介绍)。在这里,您的代码是调用 queue.enqueue(...) 的提供方。

在旧版 Admin SDK 中,扩展程序需要将自己的扩展程序实例 ID 作为第二个参数传递,以定位同一扩展程序中的任务队列函数。从 firebase-admin 14.2.0 开始,此操作既不是必需的,也不推荐。现在,Task Queue API 默认以同一上下文(例如扩展程序或套件)中的任务队列为目标。无论是作为扩展程序还是作为独立函数,在代码中移除此参数都是安全的,并且我们鼓励这样做。移除此参数可确保可移植性和向前兼容性。

入队调用的所有其他方面(locations/<region>/functions/<name> 资源路径、任务载荷和重试逻辑)保持不变。

如需详细了解如何使用 Cloud Tasks 将函数加入队列,请参阅使用 Cloud Tasks 将函数加入队列。

之前。第 1 代扩展程序:

import { getFunctions } from "firebase-admin/functions";

const queue = getFunctions().taskQueue(
  `locations/${config.location}/functions/syncBigQuery`,
  process.env.EXT_INSTANCE_ID, // extension instance ID, injected by the runtime
);
await queue.enqueue(taskData);

之后。第 2 代扩展程序:

import { getFunctions } from "firebase-admin/functions";

const queue = getFunctions().taskQueue(
  `locations/${process.env.FUNCTION_REGION}/functions/syncBigQuery`
);
await queue.enqueue(taskData);

如果您的入队调用以带前缀的代码库为目标,则发现的函数名称也会带前缀(例如 orders-syncBigQuery);请参阅查看并安装替换函数套件实例和作为函数套件进行测试。

6. 声明所需的 API 和 IAM 角色

将扩展程序的 IAM 和 API 要求从 extension.yaml 移至代码中:

import { requiresAPI, requiresRole } from "firebase-functions";

requiresAPI("bigquery.googleapis.com", "Needed to write changelog rows");
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/bigquery.user");

借助声明式安全机制,Firebase CLI 会为代码库创建或更新代管式运行时服务账号,并向其授予所有声明角色的并集。为用户提供文档,说明代码库中的所有函数都使用这些角色运行,除非最终 API 支持更窄的模型。

实际示例:从 Cloud Firestore 流式传输到 BigQuery

之前。在 extension.yaml 中声明;Extensions 运行时启用了该 API,并向管理的账号授予了角色:

apis:
  - apiName: bigquery.googleapis.com
roles:
  - role: bigquery.dataEditor
  - role: datastore.user
  - role: bigquery.user

之后。使用 requiresAPI 和 requiresRole 在代码中声明:

import { requiresAPI, requiresRole } from "firebase-functions";

requiresAPI(
  "bigquery.googleapis.com",
  "Needed to write changelog rows and views"
);
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/datastore.user");
requiresRole("roles/bigquery.user");

如果您的扩展程序发布 Eventarc 事件,您还需要设置发布事件所需的适当角色和 API。此操作之前由扩展程序处理,无需在 extensions.yaml 中进行任何更改。您可以在代码中有条件地执行此操作,以便该套件仅在使用自定义 Eventarc 渠道时请求这些权限。

if (!!process.env.EVENTARC_CHANNEL) {
  requiresRole("roles/eventarc.publisher");
  requiresAPI(
    "eventarcpublishing.googleapis.com",
    "Publishes the extension's custom events to its Eventarc channel."
  );
}

7. 转换生命周期钩子

如果您的扩展程序调用 getExtensions().runtime()(例如 setProcessingState 或 setFatalError),请删除这些调用,因为如果从正常部署的第 2 代函数调用,它们会抛出错误。生命周期状态现在由 afterFirstDeploy 和 afterRedeploy 驱动,其中不使用此状态跟踪。

Firebase Extensions 可以在用户安装、更新或重新配置扩展程序时运行设置。在 npm 软件包中,在代码中声明等效的生命周期操作。

对于一次性设置:

import { afterFirstDeploy } from "firebase-functions/lifecycle";
import { onTaskDispatched } from "firebase-functions/tasks";

export const runInitialSetup = onTaskDispatched(async (request) => {
  await initializeResources(request.data);
});

afterFirstDeploy({
  task: {
    function: "runInitialSetup",
    body: {}
  }
});

对于配置或代码更新:

import { afterRedeploy } from "firebase-functions/lifecycle";

afterRedeploy({
  task: {
    function: "runInitialSetup",
    body: { reconcile: true }
  }
});

使生命周期操作具有幂等性。如果分派或执行失败,您的用户可能需要手动重新运行这些任务:

firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME
firebase functions:lifecycle:run afterRedeploy CODEBASE_NAME

实际示例:从 Cloud Firestore 流式传输到 BigQuery

之前。extension.yaml中的 lifecycleEvents,由 Extensions 运行时驱动:

lifecycleEvents:
  onInstall:
    function: initBigQuerySync
    processingMessage: Configuring BigQuery Sync.
  onUpdate:
    function: setupBigQuerySync
    processingMessage: Configuring BigQuery Sync
  onConfigure:
    function: setupBigQuerySync
    processingMessage: Configuring BigQuery Sync

之后。在代码中声明;任务在首次部署时预配 BigQuery:

import { afterFirstDeploy, afterRedeploy } from "firebase-functions/lifecycle";

afterFirstDeploy({ task: { function: "initBigQuerySync" } });
afterRedeploy({ task: { function: "setupBigQuerySync" } });

预配是幂等的,因此重新运行会协调数据集、表和视图。 用户可以使用 firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME 手动重新运行。

8. 为用户设置文档

编写一个软件包 README,其中至少说明:

  • 软件包所需的 .env 值。
  • 软件包所需的 Secret,以及如何迁移现有 Secret 值。
  • 软件包使用 requiresRole(...) 声明的 IAM 角色。
  • 软件包启用或需要的 Google API。
  • 软件包声明的生命周期钩子,以及如何手动重新运行它们。
  • 结算备注。
  • 与原始扩展程序相比,有哪些变化。
  • 软件包如何获取其实例 ID(由 CLI 设置的 FIREBASE_KIT_INSTANCE_ID),以及实例的所有函数都使用 kit-<instanceId>- 前缀进行部署。

实际示例:从 Cloud Firestore 流式传输到 BigQuery

软件包 README 附带一个具体的“变更内容”表:

问题 作为扩展程序 As @firebase-function-kits/firestore-bigquery-export
配置 扩展程序参数 Cloud Functions 个参数(通过 .env)
IAM 由扩展程序授予 requiresRole(...),在部署时应用
正在预配 扩展程序生命周期任务 afterFirstDeploy/afterRedeploy 项任务
函数名称 ext-<instanceId>-fsexportbigquery fsexportbigquery(可选择添加前缀)
实例 ID 由扩展程序注入的 EXT_INSTANCE_ID FIREBASE_KIT_INSTANCE_ID,由 CLI 通过 firebase.json 设置

9. 测试第 2 代函数

现在,您应该有一个第 2 代函数,该函数在部署后,其行为与新安装的扩展程序完全相同。下一步是验证并修复在此过程中意外引入的所有问题。

如需调用 setGlobalOptions 来设置全局选项(例如默认区域或 CPU),您只能在将套件部署为独立的第 2 代函数时执行此操作。当您的套件作为 npm 软件包安装时,用户会在其封装代码中调用 setGlobalOptions 来配置这些参数,如果此操作发生两次,他们会收到警告。您可以通过检查 FIREBASE_KIT_INSTANCE_ID 环境变量来保护此调用:

import { setGlobalOptions } from "firebase-functions";

if (!process.env.FIREBASE_KIT_INSTANCE_ID) {
  setGlobalOptions({
    region: "us-east1",
    maxInstances: 10,
  });
}

确保您使用的是 firebase-tools >= 15.32.0,并将转换后的第 2 代函数部署到具有适当资源的测试项目中,以测试其行为。如果您已经设置了用于测试扩展程序的测试项目,请运行以下命令:

firebase deploy --only functions

按照在 Firebase 控制台中填写扩展程序安装表单的方式,填写提示您输入参数值的向导。

实际示例:从 Cloud Firestore 流式传输到 BigQuery

我们对 Cloud Firestore 到 BigQuery 的同步进行了端到端验证:

  1. 在 Firebase 控制台的 Cloud Firestore 页面中,创建您设置为 COLLECTION_PATH (users) 的集合(如果尚不存在)。
  2. 创建一个名为 bigquery-mirror-test 的文档,其中包含具有任意值的任意字段。
  3. 在 Google Cloud 控制台的 BigQuery 页面中,查询原始变更日志表。它应该包含一行记录文档创建的日志:

    SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
    
  4. 查询最新视图,该视图应返回唯一存在的文档 (bigquery-mirror-test) 的最新更改事件:

    SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
    
  5. 删除 Cloud Firestore 中的 bigquery-mirror-test 文档。它会从最新视图中消失,并且 DELETE 事件会附加到原始变更日志表中。

    您可以使用以下命令检查单个文档的完整历史记录:

    SELECT *
       FROM `PROJECT_ID.analytics.users_raw_changelog`
       WHERE document_name = "bigquery-mirror-test"
       ORDER BY timestamp ASC
    

与测试扩展程序的区别:

  • 触发器会部署为 fsexportbigquery(作为典型的第 2 代函数而非套件部署时没有前缀),而不是 ext-<instanceId>-fsexportbigquery。在 Cloud Functions 信息中心和日志中查找该名称。
  • 现在,您的代码会在 Firebase Local Emulator Suite 中作为普通函数运行。 您可以使用 .env.local 设置要在模拟器中使用的参数值。您还可以使用 firebase-functions-test SDK 对代码进行单元测试,如Cloud Functions 的单元测试中所述。
  • 配置不再由 Extensions 运行时驱动。如果部署后缺少变更日志表,请手动重新运行设置任务:firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME。该任务是幂等的,因此重新运行该任务会协调数据集、表和视图。
  • 参数值来自 .env,而不是安装表单,因此在 .env 完成后,重新运行 firebase deploy 是非交互式的。

10. 在 npm 上发布函数套件

验证从扩展程序到第 2 代函数的转换后,您可以按照以下指南之一将候选版本发布到 npm,以进行端到端测试:

将套件发布到 npm 后,可以使用 firebase functions:kits:install 进行安装,并列为扩展程序的官方替代项。

我们强烈建议您先发布候选版本。套件是按软件包名称和版本安装的,因此预发布版本可让您针对注册表测试实际安装流程,而不会向使用默认 latest 标记安装的用户公开未完成的软件包。

发布前:

  1. 选择软件包名称。限定范围和不限定范围的名称均可使用(请参阅上述指南)。请注意,作用域包默认是私有的,因此请传递 --access public。
  2. 构建并检查发布的内容。main 和 types 指向编译后的输出(在我们的工作示例中为 lib/),因此该目录必须包含在发布的 tar 文件中。使用 .npmignore 或 files 许可名单,并使用 npm pack --dry-run 检查结果。运行 build 的 prepublishOnly 脚本可防止发布过时的输出。

package.json 项新增功能,默认确保发布安全:

{
  "files": ["lib", "README.md", "CHANGELOG.md"],
  "publishConfig": { "access": "public", "tag": "next" },
  "scripts": {
    "build": "tsc -b",
    "prepublishOnly": "npm run build && npm test"
  }
}

"publishConfig": { "tag": "next" } 字段可确保纯 npm publish 绝不会覆盖 latest。

创建候选版本:

例如,如需在本地将版本从 0.0.2-rc.3 递增到 0.0.2-rc.4(如果 package.json 位于代码库根目录,则会在 Git 中提交和标记),请运行以下命令:

npm version prerelease --preid rc

如需发布候选版本并在 npm 上使用 next 标记注册 @your-org/your-kit@0.0.2-rc.4,请运行以下命令:

npm publish

npm 网站可能需要几分钟才能显示新版本;npm view 会直接读取注册表:

npm view @your-org/your-kit versions dist-tags

完成本指南的第 11 步和第 12 步后,您可以将软件包升级为稳定版:

npm version 0.0.2
npm publish --tag latest
npm dist-tag add @your-org/your-kit@0.0.2 next

关于 npm-shrinkwrap.json 的说明:我们强烈建议您在软件包中包含 npm-shrinkwrap.json 文件;如果您不这样做,CLI 会在用户安装时发出警告。它可确保用户使用您测试过的确切依赖项,并有助于防范供应链攻击。不过,shrinkwrap 会逐字逐句地应用到用户的项目中,包括在 Cloud Functions build (npm ci) 期间,其中仅限开发环境的条目可能会失败并显示 EBADPLATFORM。您可能需要从已发布的 shrinkwrap 副本中剥离 "dev": true 条目和 devDependencies。

实际示例:从 Cloud Firestore 流式传输到 BigQuery

该套件在第四个候选版本发布时的 package.json:

{
  "name": "@firebase-function-kits/firestore-bigquery-export",
  "version": "0.0.2-rc.4",
  "repository": {
    "type": "git",
    "url": "https://github.com/firebase/extensions.git",
    "directory": "kits/firestore-bigquery-export"
  },
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "engines": { "node": "22" },
  "scripts": { "build": "tsc -b" }
}

请注意,在这种情况下,该套件位于 monorepo 中,因此请务必添加 repository.directory,以便 npm 注册表链接指向正确的文件夹。其 CHANGELOG.md 包含待发布版本的说明。

11. 作为函数套件进行测试

发布套件后,我们建议您使用 npm 测试套件。

确保您使用的是 firebase-tools 版本 >= 15.32.0,并安装该套件:

firebase functions:kits:install --package <your-package-name>@<your-prerelease-version>

此命令会从 npm 下载您的软件包,在套件的新源目录中进行设置,并引导您完成第一个实例的配置,类似于扩展程序安装流程。在本地安装并设置软件包后,运行部署以在 Google Cloud 项目中创建资源:

firebase deploy --only functions:<your-kit-instance-id>

安装后,Firebase CLI 会输出类似的部署命令,其中包含您在安装期间选择的确切实例 ID。

按照第 9 步测试第 2 代函数。现在,您可以使用套件进行部署,您的函数会带有前缀并命名为 kit-<instance-id>-<method-name>。这样,套件就可以有多个实例,在项目中多次部署同一函数,每个实例都有一个唯一的名称。

12. 测试迁移替换

您可以设置一个可正常运行的扩展程序实例,然后使用用户迁移指南(使用 firebase ext:migrate --package 或函数套件 CLI 命令)完成函数套件的测试,以作为迁移替代方案。

13. 向用户和 Google 通知您的官方扩展程序替代方案

当您的函数套件替代方案准备就绪,并作为 npm 软件包提供给用户迁移时,请将此官方替代方案告知用户和 Google。使用以下信息更新托管扩展程序的 GitHub 代码库中的 README.md 文件:

<!-- FIREBASE_EXTENSION_REPLACEMENT: extension="<your-extesion-id>" package="<your-npm-package-name>" -->
> [!WARNING]
> **Deprecation Notice:** The Firebase Extension `<your-extension>` is deprecated. Migrate to the [<your-npm-package-name>](<link-to-your-npm-package>) package.

Google 会扫描已知扩展程序的 README 文件,查找 <!-- FIREBASE_EXTENSION_REPLACEMENT: extension="firebase/firestore-bigquery-export" package="@firebase-function-kits/firestore-bigquery-export" --> 等注释,并使用这些注释填充存储在 firebase-tools 代码库中的官方替换项注册表,路径为 replacements.json。您还可以查看 replacements.json,了解系统会针对您的扩展程序扫描哪些 README.md。官方更换件列表每周更新一次。

实际示例:从 Cloud Firestore 流式传输到 BigQuery

firestore-bigquery-export 扩展程序 README.md 包含:

<!-- FIREBASE_EXTENSION_REPLACEMENT: extension="firebase/firestore-bigquery-export" package="@firebase-function-kits/firestore-bigquery-export" -->
> [!WARNING]
> **Deprecation Notice:** The Firebase Extension `firebase/firestore-bigquery-export` is deprecated. Please migrate to the [`@firebase-function-kits/firestore-bigquery-export`](https://www.npmjs.com/package/@firebase-function-kits/firestore-bigquery-export) package.