extension-to-functions-codebase

extension-to-functions-codebase

热门

将已安装的 Firebase 扩展(或扩展源代码)转换为独立的 Cloud Functions for Firebase 代码库或可发布的 npm 包,包括将触发器从 V1 升级到 V2,以及配置生命周期钩子和声明式安全性的技能。

394Star
79Fork
更新于 2026/7/30
SKILL.md
readonly只读
name
extension-to-functions-codebase
description

将已安装的 Firebase 扩展(或扩展源代码)转换为独立的 Cloud Functions for Firebase 代码库或可发布的 npm 包,包括将触发器从 V1 升级到 V2,以及配置生命周期钩子和声明式安全性的技能。

扩展到 Functions 代码库及 npm 包迁移

概述

本技能指导代理将 Firebase 扩展仓库或实例迁移为以下之一:

  1. 独立的 Cloud Functions for Firebase 代码库firebase-functions,用于最终用户应用集成),或
  2. 可发布的 npm 包 / 可共享的开源包(用于分发可复用 V2 函数的扩展发布者)。

它利用 Cloud Functions for Firebase 的正式可用(GA)能力,在代码中原生处理权限、依赖和生命周期钩子,并提供使用解构兼容垫片(Destructuring Compatibility Shim)将旧版 V1 触发器升级到 V2 的说明。


触发器

当用户请求以下操作时激活此技能:

  • 将已安装的 Firebase 扩展迁移或转换为独立的 functions 代码库。
  • 将扩展仓库转换为可发布的 npm 包(可共享的开源包)。
  • 将扩展触发器从 V1 升级到 V2。

目标迁移工作流

开始之前,与开发者确定目标目的地:

  • 目标 A:本地 Functions 代码库(最终用户应用集成)

    • 输出:源代码放置在项目的 functions/src/ 文件夹中。
    • 配置:通过 defineStringdefineSecret 等定义参数,并放在 .env 中。
    • 部署:直接通过 firebase deploy --only functions 部署。
  • 目标 B:可发布的 npm 包 / 可共享的开源包

    • 输出:包含导出的 V2 函数的可复用 npm 包。
    • 配置:package.json 中包含 exports 映射,并在 dependencies(或 peerDependencies)中声明 firebase-functions
    • 使用:最终用户安装该包(npm i <package-name>)并在其 index.ts 中重新导出函数。

开始与 Git 安全

  1. Git 状态:开始前验证工作区具有干净的 git 状态。
  2. 就地复制:如果在同一仓库内复制代码到新的子目录:
    • 使用 git cp(或复制文件并提交)将扩展的源目录复制到目标目录。
    • 立即提交:
      "Copying [extension-name] extension to [directory] in preparation for rewrite"

规则与约束

1. 零本地开销(Cloud Functions 集成)

假设 Cloud Functions for Firebase 工作负载身份、声明式安全性和 SDK 生命周期钩子已完全正式可用(GA)。

  • 不要输出指示用户运行手动 gcloud IAM 命令或创建服务账户的说明或脚本。
  • 不要编写指示用户在云控制台中手动启用 Google API 的代码注释。
  • 相反,使用 SDK 中的声明式 requiresAPIrequiresRole 导入。

2. 全局参数访问限制

  • 切勿在全局作用域中对任何参数调用 .value()
  • 如果使用参数值初始化全局变量或类实例,请在全局声明变量,并在 onInit() 回调中初始化:
    import { defineString } from "firebase-functions/params";
    import { onInit } from "firebase-functions/v2";
    
    const bqDataset = defineString("DATASET_ID");
    let bqClient: BigQuery;
    
    onInit(() => {
      bqClient = new BigQuery({ datasetId: bqDataset.value() });
    });
    

3. V2 的并发与成本对等

升级触发器到 V2 时:

  • 默认情况下,V2 函数启用并发(每个实例最多 80 个请求)。
  • 如果要保持 V1 的分级 CPU 定价(并禁用并发),请在函数的选项对象中设置 cpu: "gcf_gen1"

逐步迁移执行

第 1 步:盘点扩展

对扩展声明、附带和文档化的所有内容进行全面盘点,以确保迁移过程中不丢失任何内容:

  1. 盘点 extension.yaml
    • params:转换为 Functions 参数(defineStringdefineSecret 等)。
    • apis:转换为 requiresAPI(...) 声明。
    • roles:转换为 requiresRole(...) 声明。
    • lifecycleEventsonInstallonUpdateonConfigure):转换为 afterFirstDeployafterRedeploy 钩子。
    • resources:记录所有需要从第 1 代(firebase-functions/v1)转换为第 2 代(firebase-functions/v2)的函数触发器,包括标准事件触发器、HTTP 处理程序和任务队列(onTaskDispatched)。
  2. 盘点文件与工具
    • functions/:源代码、触发器、辅助函数和任务队列处理程序。
    • 文档:README.mdPREINSTALL.mdPOSTINSTALL.md
    • scripts/:记录扩展附带的任何回填、导入或辅助脚本。

第 2 步:创建 / 更新 package.json

为迁移的扩展代码创建 npm 包(在项目根目录或专用工作区目录):

  • 设置可发布的包名(name: "<package-name>")。
  • 保留开发与测试依赖:保留旧扩展(functions/package.json 或根 package.json)中所有现有的 devDependencies、测试运行器(jestts-jest@types/jestmocha@types/mocha)和测试脚本("test": "...")。不要丢弃测试框架或类型定义。
  • dependencies(或 peerDependencies,如果创建轻量级中间件包,由根消费者管理运行时版本)中声明 firebase-functions(例如 ^7.0.0):
    {
      "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"
      },
      "dependencies": {
        "firebase-admin": "^12.0.0",
        "firebase-functions": "^7.0.0"
      }
    }
    

第 3 步:移动函数代码并暴露可部署函数

将扩展的函数源代码移动到包的 src/ 文件夹中:

  1. 保留正常的 Firebase 触发器导出。包必须暴露可部署的函数,最终用户可以从其入口点(index.ts)重新导出。
  2. 记录消费者必须通过重新导出来部署打包函数:
    export * from "<package-name>";
    // 或命名导出:
    // export { syncV2, initBigQuerySync } from "<package-name>";
    
    注意:仅裸导入(import "<package-name>")是不够的。Firebase CLI 只部署从用户根入口文件导出的函数。

第 4 步:将函数从第 1 代升级到第 2 代

将每个导出的第 1 代函数触发器(onWriteonRequesttasks.taskQueue().onDispatch)转换为对应的第 2 代触发器(onDocumentWrittenonRequestonTaskDispatched,来自 firebase-functions/v2/...):

  1. 签名与解构垫片:使用解构兼容垫片({ shimmedKey, context })保留 V1 业务逻辑,无需重写函数体。参见 signature-mapping.mddestructuring-shim.md
  2. 身份验证触发器例外:如果您的扩展使用 v1 身份验证触发器(auth.user().onCreate()auth.user().onDelete()),指示代理实时检查已安装的 firebase-functions 包或实时文档中是否存在第 2 代替代方案。如果这些事件尚不支持第 2 代 Auth 触发器,请明确警告用户,并在 V2 替代方案可用之前暂停或拒绝这些特定触发器的迁移。

第 5 步:用 Functions 参数替换扩展参数

extension.yaml 中的每个参数都变为参数化配置调用:

  1. 映射参数原始类型和属性:
    • 类型 string ->
      defineString("PARAM_NAME", { label: "...", description: "...", default: "..." })
    • 类型 secret -> defineSecret("PARAM_NAME")
    • 类型 int -> defineInt("PARAM_NAME", { label: "...", default: 123 })
    • 类型 boolean -> defineBoolean("PARAM_NAME", { default: true })
    • 类型 select / multiSelect -> 将 options 映射到 input
      defineString("PARAM_NAME", { input: { select: { options: [{ value: "val", label: "Val" }] } } })
    • validationRegex -> 映射到文本输入选项:
      defineString("PARAM_NAME", { input: { text: { validationRegex: "^[a-z]+$" } } })
    • required: true / 非空验证 -> 将 nonEmpty: true 映射到输入选项:
      defineString("PARAM_NAME", { input: { text: { nonEmpty: true } } })
  2. 在处理程序内部使用 paramName.value() 读取参数值。
  3. 保持参数名称不变:切勿更改参数名称(COLLECTION_PATHDATASET_ID 等),以便现有值在 .env 中无缝延续。

第 6 步:迁移内部任务队列调用(queue.enqueue(...)

如果您的扩展代码使用 Admin SDK(getFunctions().taskQueue(...))将任务入队到自己的队列:

  • 在扩展运行时下,Admin SDK 通过函数名称加上 process.env.EXT_INSTANCE_ID 解析队列。
  • 在 npm 包 / 常规代码库中:完全移除第二个参数(EXT_INSTANCE_ID)。Admin SDK 自动定位当前代码库:
    // 之前(扩展运行时):
    // const queue = getFunctions().taskQueue(`locations/${region}/functions/syncBigQuery`, process.env.EXT_INSTANCE_ID);
    
    // 之后(npm 包):
    const queue = getFunctions().taskQueue(`locations/${region}/functions/syncBigQuery`);
    await queue.enqueue(taskData);
    

第 7 步:迁移密钥

对于声明为 type: secret 的参数:

  1. 显式声明密钥:
    import { defineSecret } from "firebase-functions/params";
    const apiKey = defineSecret("API_KEY");
    
  2. 在触发器选项中绑定密钥:
    export const fn = onRequest({ secrets: [apiKey] }, handler);
    
  3. 保持密钥名称不变,以便现有的 Secret Manager 绑定正常工作。

第 8 步:声明所需的 API 和 IAM 角色

用入口文件中的声明式代码替换 extension.yaml 中的 apisroles

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

requiresAPI("bigquery.googleapis.com", "需要写入变更日志行");
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/bigquery.user");

部署时,Firebase CLI 自动将这些角色授予托管运行时服务账户,并启用所需的 API。

第 9 步:转换生命周期钩子(afterFirstDeployafterRedeploy

用 SDK 生命周期钩子替换 lifecycleEventsonInstallonUpdateonConfigure):

  1. 将任务队列处理程序(initBigQuerySyncsetupBigQuerySync)转换为来自 firebase-functions/v2/tasks 的 V2 onTaskDispatched(移除旧的 getExtensions().runtime().setProcessingState(...) 调用)。
  2. 在代码中注册生命周期钩子:
    import { afterFirstDeploy, afterRedeploy } from "firebase-functions/v2";
    
    // 替换 onInstall:
    afterFirstDeploy({
      task: {
        function: "runInitialSetup",
        body: {}
      }
    });
    
    // 替换 onUpdate 和 onConfigure:
    afterRedeploy({
      task: {
        function: "runInitialSetup",
        body: { reconcile: true }
      }
    });
    
  3. 确保处理程序是幂等的,并记录手动重新运行的命令:
    firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME
    firebase functions:lifecycle:run afterRedeploy CODEBASE_NAME
    

第 10 步:为用户记录设置(README.md

保留原始扩展在 README.md(以及 PREINSTALL.md / POSTINSTALL.md)中已记录的任何文档、密钥和设置说明,并根据需要进行更新:

  1. 更新配置引用:将引用旧版 extension.yaml 安装提示或 ext-*.env 文件的说明替换为与 defineString 参数匹配的标准 .env 参数化配置设置。
  2. 包含重新导出代码片段:确保显示基本的根重新导出片段(export * from "<package-name>";),以便用户知道如何在根 index.ts 中暴露函数。
  3. 避免不必要的样板:如果设置已在代码中显而易见或已由现有 README 部分覆盖,则不要生成冗余的比较表或通用安装步骤。

第 11 步:构建与测试验证

  1. 验证源代码编译:运行 npm run buildtsc)以确保 src/ 中没有 TypeScript 编译错误。
  2. 验证单元测试套件和类型定义
    • 如果扩展中存在现有单元测试(__tests__/test/):
      • 确保 @types/jest(或原始测试框架类型)存在于 devDependencies 中,以便测试文件干净地通过类型检查。
      • 更新升级后的 V2 触发器的测试调用,以传递单个解构事件对象
        ({ change: mockChange, context: mockContext }) 而不是位置参数 (mockChange, mockContext))。
      • 运行 npm test 或对测试文件进行类型检查(npx tsc --noEmit)以验证零回归。

参考与官方资源