以发布者的身份将扩展程序迁移到 Cloud Functions

本指南介绍了如何将扩展程序从已弃用的 Firebase Extensions 环境迁移到用户在自己的 Cloud Functions 中安装和部署的函数,以用于 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 步(清点资源、触发升级、参数和密钥转换、声明式 IAM、生命周期钩子以及生成软件包 README)。

安装技能

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

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

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

“请按照 extension-to-functions-codebase 技能中的说明,将此 Firebase 扩展程序迁移到可发布的第 2 代函数套件软件包中。”

1. 清点扩展程序

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

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

  • extension.yaml,用于声明参数、函数、事件、IAM 角色、必需的 API、密钥和生命周期钩子。
  • functions/,其中包含函数代码、依赖项、构建配置、触发器和任务队列函数。
  • README.md、PREINSTALL.md 和 POSTINSTALL.md,其中包含设置步骤、警告和结算备注。
  • scripts/,其中包含任何导入、回填、IAM、修复或迁移实用程序,以及您随扩展程序一起提供的任何其他工具。

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

  • 将用户配置转换为 Cloud Functions 参数(第 4 步)。
  • 将密钥转换为 Cloud Functions 密钥(第 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 映射和 firebase-functions 已移至 peerDependencies:

{
  "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

参数

您在 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。函数套件从 FIREBASE_KIT_INSTANCE_ID 读取其实例 ID,Firebase CLI 会将每个套件实例的 设置为 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 到 BigQuery 扩展程序不会读取其实例 ID,因此无需迁移任何内容。“删除用户数据”扩展程序使用它来命名其 Pub/Sub 主题。)

之前。在 config.ts 中读取为原始环境变量,带有扩展程序用于其资源的 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,也不需要在多个实例之间保持同步。