En esta guía, se muestra cómo migrar tus extensiones del entorno obsoleto de Firebase Extensions a una función que los usuarios instalan y, luego, implementan en su propia Cloud Functions para Firebase (2ª gen.).
Esta es la ruta de migración recomendada. Firebase mantendrá una lista de extensiones con equivalentes oficiales de npm. En esta guía, se te guiará para crear la tuya.
En esta guía, se usa como ejemplo la extensión Stream Firestore to BigQuery (firestore-bigquery-export). Cada sección termina con un ejemplo práctico que muestra cómo era la extensión antes de la migración y cómo es después, como el paquete @firebase/firestore-bigquery-export.
Regístrate para obtener más información y ayuda sobre la migración de extensiones
Si tienes preguntas sobre cómo migrar desde Firebase Extensions, puedes comunicarte con nosotros a firebase-extensions-migrator-support-external@google.com. También enviaremos un correo electrónico a este grupo a medida que actualicemos la guía con más información sobre el empaquetado, las pruebas y la distribución de tus funciones de 2ª gen.
Para unirte a este grupo, envía un mensaje a firebase-extensions-migrator-support-external+subscribe@google.com, que responderá con un correo electrónico de solicitud de membresía. Debes responder ese correo electrónico, no hacer clic en el botón "Unirse a este grupo".
Antes de comenzar
Para completar esta migración, usarás las siguientes funciones de Cloud Functions:
Configuración con parámetros. Cada parámetro que declares en
extension.yamlse convierte en un parámetro definido en el código de tu paquete.Roles de IAM declarativos y APIs obligatorias. Cada rol que declares en
extension.yamlse convierte en una llamadarequiresRole(...), y cada API se convierte en una llamadarequiresAPI(...)en el código de tu función. En el momento de la implementación, la Firebase CLI otorga los roles declarados a una cuenta de servicio de entorno de ejecución administrado y habilita las APIs declaradas en tu nombre.Eventos de ciclo de vida para bases de código Cloud Functions. Cloud Functions Las bases de código ahora admiten eventos de ciclo de vida análogos a Firebase Extensions. Declara la configuración de tiempo de instalación y de actualización con los hooks de ciclo de vida
afterFirstDeploy(...)yafterRedeploy(...). Estos reemplazan loslifecycleEventsque declaras enextension.yaml.
Inventario de la extensión
Comienza por hacer un inventario de tu extensión: una lista completa de todo lo que la extensión declara, envía y documenta, de modo que cada comportamiento tenga un destino definido en la función de 2ª gen. y no se pierda nada en la migración.
Revisa cada uno de los siguientes elementos y toma nota de lo que encuentres:
extension.yaml, que declara tus parámetros, funciones, eventos, roles de IAM, APIs obligatorias, secretos y hooks de ciclo de vidafunctions/, que contiene el código de tu función, las dependencias, la configuración de compilación, los activadores y las funciones de la lista de tareas en colaREADME.md,PREINSTALL.mdyPOSTINSTALL.md, que contienen pasos de configuración, advertencias y notas de facturaciónscripts/, que contiene cualquier utilidad de importación, relleno, IAM, reparación o migración, y cualquier otra herramienta que envíes junto con la extensión
Luego, para cada elemento de
extension.yaml, decide a dónde
va en el paquete npm:
Convierte la configuración del usuario en Cloud Functions parámetros (sección 5).
Convierte los secretos en Cloud Functions secretos (sección 6).
Convierte los roles de IAM en declaraciones
requiresRole(...)(sección 8).Convierte las APIs de Google obligatorias en declaraciones
requiresAPI(...)cuando corresponda (sección 8).Convierte los hooks de instalación y actualización en declaraciones
afterFirstDeploy(...)yafterRedeploy(...)(sección 8).
Ejemplo práctico: Stream Firestore to BigQuery
La lectura de firestore-bigquery-export/extension.yaml y functions/ produce este inventario:
| En extension.yaml | Cantidad / valor | A dónde va |
|---|---|---|
| params | 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) | Cloud Functions parámetros (sección 5) |
| apis | bigquery.googleapis.com | requiresAPI(...) (sección 7) |
| roles | bigquery.dataEditor, datastore.user, bigquery.user | requiresRole(...) (sección 7) |
| recursos | 1 activador de eventos (fsexportbigquery) + funciones de la lista de tareas en cola (initBigQuerySync, setupBigQuerySync) | Funciones de paquetes exportados (sección 3) |
| lifecycleEvents | onInstall → initBigQuerySync; onUpdate / onConfigure → setupBigQuerySync | afterFirstDeploy / afterRedeploy (sección 9) |
| scripts/ | import/ (relleno), gen-schema-view/ | Se conservan como secuencias de comandos (fuera del alcance aquí) |
La extensión no declara ningún tipo: parámetros secretos, por lo que no hay nada que migrar en la sección 6 de esta guía. El activador de eventos ya es de 2ª gen.; solo las funciones de la lista de tareas en cola siguen siendo de 1ª gen. (relevante en la sección 3).
Actualiza package.json
Actualiza el archivo package.json de tu extensión. Si migras una extensión, puede ser la raíz package.json. Si migras muchas extensiones en un repositorio, asigna a cada extensión su propio paquete.
Versiones mínimas del SDK. Declara firebase-functions >= 7.3 y firebase-admin >= 14.2.0 como dependencias. Declara también tu versión de firebase-functions como una dependencia de pares.
{
"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.3.0"
},
"dependencies": {
"firebase-functions": "^7.3.0",
"firebase-admin": "^14.2.0"
}
}
Declara firebase-functions como una dependencia de pares, además de tu dependencia normal , de modo que el proyecto de Cloud Functions de tus usuarios tenga la misma versión del SDK con la que se escribió tu biblioteca.
Ejemplo práctico: Stream Firestore to BigQuery
Antes. El archivo functions/package.json de la extensión es privado, nombra el ID de la extensión y declara firebase-functions como una dependencia directa:
{
"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"
}
}
Después. Un paquete publicable: nombre con alcance, un mapa de exportaciones y firebase-functions movido a peerDependencies:
{
"name": "@firebase/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.3.0" },
"dependencies": {
"@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
"firebase-admin": "^14.2.0",
"firebase-functions": "^7.3.0"
}
}
Actualiza las funciones de 1ª gen. a 2ª gen.
Si tu extensión aún exporta funciones de 1ª gen., convierte cada función a su equivalente de 2ª gen. Importa desde los módulos firebase-functions/... y pasa la configuración del entorno de ejecución en las opciones de la función.
Puedes minimizar los esfuerzos de reescritura con la desestructuración de eventos parcheados de 2ª gen. y evitar reescribir la lógica de tu función ya que el SDK de 2ª gen. ahora expone los parámetros V1 como campos en el objeto de evento, lo que te permite usar parámetros desestructurados o con nombre y mantener la lógica empresarial sin cambios.
Antes. 1ª gen.:
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);
});
Después. 2ª gen.:
import { onDocumentWritten } from "firebase-functions/firestore";
export const syncV2 = onDocumentWritten(
{ document: "{collectionId}/{documentId}" },
async ({change,context}) =>
await handleWrite(change.before, change.after, context.params);
);
Consulta la Cloud Functions comparación de versiones para obtener una lista completa de las diferencias entre las funciones de 1ª gen. y 2ª gen.
Convierte parámetros y secretos de extensión
Convierte parámetros
Cada parámetro que declares en extension.yaml se convierte en un Cloud Functions
parámetro.
Convierte las lecturas directas del entorno:
const collectionPath = process.env.COLLECTION_PATH;
en Cloud Functions parámetros:
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);
}
);
Usa collectionPath.value() para leer la cadena dentro de un controlador; usa
collectionPath directamente cuando se espera un marcador de posición, como una ruta de acceso de activador de función.
La Firebase CLI descubre tus parámetros y lee sus valores de .env, .env.projectId, o solicita a tus usuarios durante la implementación. Mantén los mismos nombres de parámetros para que los valores de una instalación existente se transfieran.
Es importante que no cambies los nombres de los parámetros declarados en tu código en absoluto. La migración de extensiones conservará automáticamente el valor del parámetro del usuario final existente, pero solo cuando los nombres no cambien.
Ejemplo práctico: Stream Firestore to BigQuery
Antes. Un parámetro declarado en extension.yaml, leído como una variable de entorno sin procesar en config.ts:
# extension.yaml
- param: COLLECTION_PATH
label: Collection path
type: string
required: true
// functions/src/config.ts
collectionPath: process.env.COLLECTION_PATH,
Después. Un defineString; la CLI lo descubre y lee desde .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, analagous to "required: true" in extensions.yaml
input: { text: { nonEmpty: true} }
}),
El nombre del parámetro no cambia, por lo que un .env existente sigue funcionando.
Convierte secretos
En extension.yaml, declaras secretos con type: secret. El entorno de ejecución de Extensiones los almacena y los vincula, de modo que el código de tu extensión pueda leer process.env.PARAM_NAME directamente. En una base de código típica de Cloud Functions, declaras y vinculas cada secreto de forma explícita:
import { defineSecret } from "firebase-functions/params";
import { onRequest } from "firebase-functions/https";
const apiKey = defineSecret("API_KEY");
export const fn = onRequest({ secrets: [apiKey] }, handler);
Una vez que tu extensión se migre a un paquete o kit de npm, las referencias secretas se administrarán en el archivo .env del usuario final. Es importante que no cambies los nombres de los secretos declarados en tu código en absoluto. Durante la migración, los secretos del usuario final se migrarán según corresponda.
Ejemplo práctico: Trigger Email From Cloud Firestore
Antes. MAIL_COLLECTION y SMTP_PASSWORD se leen como variables de entorno sin procesar en 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,
Después. Un defineString y un defineSecret; la CLI descubre ambos y lee desde .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();
// ...
}
);
Migra las llamadas internas de la lista de tareas en cola
Algunas extensiones ponen en cola el trabajo en sus propias listas de tareas en cola desde el interior de su código de función, con el Firebase Admin SDK. Esto es diferente a recibir una tarea despachada (que se aborda en las secciones Actualiza funciones y Convierte hooks de ciclo de vida). Aquí, tu código es el productor que llama a queue.enqueue(...).
Las versiones anteriores de Admin SDK requerían que las extensiones pasaran su propio ID de instancia de extensión como un segundo parámetro para orientar una función de la lista de tareas en cola en la misma extensión. A partir de `firebase-admin` 14.2.0, esto no es obligatorio ni recomendado. La API de la lista de tareas en cola ahora se orientará a las listas de tareas en cola en el mismo contexto (p.ej., extensión) de forma predeterminada. Es seguro y recomendable quitar este parámetro en tu código como extensión y como funciones independientes. Quitar este parámetro garantiza la portabilidad y la compatibilidad con versiones posteriores.
Todo lo demás sobre la llamada enqueue (la ruta de acceso del recurso de ubicaciones/region/funciones/name, la carga útil de la tarea y la lógica de reintento) sigue siendo lo mismo.
Consulta /docs/functions/task-functions para obtener más detalles sobre las funciones de puesta en cola con Cloud Tasks.
Antes. Extensión de 1ª gen.
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);
Después. Extensión de 2ª gen.
import { getFunctions } from "firebase-admin/functions";
import { region } from "firebase-functions/params";
const queue = getFunctions().taskQueue(
`locations/${region.value()}/functions/syncBigQuery`);
await queue.enqueue(taskData);
Si tu llamada enqueue se orienta a una base de código con prefijo, el nombre de la función descubierta también tiene un prefijo (por ejemplo, orders-syncBigQuery).
Declara las APIs y los roles de IAM obligatorios
Mueve los requisitos de IAM y de la API de tu extensión de extension.yaml al código:
import { requiresAPI, requiresRole } from "firebase-functions"
requiresAPI("bigquery.googleapis.com", "Needed to write changelog rows");
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/bigquery.user");
Con la seguridad declarativa, la Firebase CLI crea o actualiza una cuenta de servicio de entorno de ejecución administrado para la base de código y le otorga la unión de todos los roles declarados. Documenta para tus usuarios que todas las funciones de la base de código se ejecutan con esos roles, a menos que la API final admita un modelo más limitado.
Ejemplo práctico: Stream Firestore to BigQuery
Antes. Declarado en extension.yaml; el entorno de ejecución de Extensiones habilitó la API y otorgó los roles a una cuenta administrada:
apis:
- apiName: bigquery.googleapis.com
roles:
- role: bigquery.dataEditor
- role: datastore.user
- role: bigquery.user
Después. Declarado en el código con requiresAPI y requiresRole:
import { requiresAPI, requiresRole } from "firebase-functions/";
requiresAPI("bigquery.googleapis.com",
"Needed to write changelog rows and views");
requiresRole("roles/biguqery.dataEditor");
requiresRole("roles/datastore.user");
requiresRole("roles/bigquery.user");
Convierte hooks de ciclo de vida
Si tu extensión llama a getExtensions().runtime(), por ejemplo
setProcessingState o setFatalError, borra esas llamadas, ya que mostrarán
un error si se llaman desde una función de 2ª gen. implementada de forma normal. El estado del ciclo de vida ahora se controla con afterFirstDeploy y afterRedeploy, donde no se usa el seguimiento de estado.
Firebase Extensions pueden ejecutar la configuración cuando un usuario instala, actualiza o vuelve a configurar una extensión. En tu paquete npm, declara acciones de ciclo de vida equivalentes en el código.
Para la configuración única:
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: {}
}
});
Para actualizaciones de configuración o código:
import { afterRedeploy } from "firebase-functions/lifecycle";
afterRedeploy({
task: {
function: "runInitialSetup",
body: { reconcile: true }
}
});
Haz que tus acciones de ciclo de vida sean idempotentes. Es posible que los usuarios deban volver a ejecutarlas de forma manual si falla el despacho o la ejecución:
firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME
firebase functions:lifecycle:run afterRedeploy CODEBASE_NAME
Ejemplo práctico: Stream Firestore to BigQuery
Antes. lifecycleEvents en extension.yaml, controlado por el entorno de ejecución de Extensiones:
lifecycleEvents:
onInstall:
function: initBigQuerySync
processingMessage: Configuring BigQuery Sync.
onUpdate:
function: setupBigQuerySync
processingMessage: Configuring BigQuery Sync
onConfigure:
function: setupBigQuerySync
processingMessage: Configuring BigQuery Sync
Después. Declarado en el código; la tarea aprovisiona BigQuery en la primera implementación:
import { afterFirstDeploy, afterRedeploy } from "firebase-functions/lifecycle";
afterFirstDeploy({ task: { function: "initBigQuerySync" } });
afterRedeploy({ task: { function: "setupBigQuerySync" } });
El aprovisionamiento es idempotente, por lo que una nueva ejecución concilia el conjunto de datos, la tabla y las vistas. Los usuarios pueden volver a ejecutar de forma manual con firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.
Documenta la configuración para tus usuarios
Escribe un
READMEdel paquete que explique, como mínimo, lo siguiente:Los valores
.envque requiere el paqueteLos secretos que requiere el paquete y cómo migrar los valores secretos existentes
Los roles de IAM que declara el paquete con
requiresRole(...)Las APIs de Google que habilita o requiere el paquete
Los hooks de ciclo de vida que declara el paquete y cómo volver a ejecutarlos de forma manual
Notas de facturación
Qué cambió en comparación con la extensión original
Ejemplo práctico: Stream Firestore to BigQuery
El archivo README del paquete envía una tabla concreta de "qué cambió":
| Problema | Como la extensión | Como @firebase/firestore-bigquery-export |
|---|---|---|
| Configuración | Parámetros de extensión | Parámetros de funciones a través de .env |
| IAM | Otorgado por Extensiones | requiresRole(...), aplicado en la implementación |
| Aprovisionamiento | Tarea de ciclo de vida por Extensiones | Tarea afterFirstDeploy / afterRedeploy |
| Nombres de las funciones | ext-instanceId-fsexportbigquery | fsexportbigquery (con prefijo opcional) |
Prueba tu función de 2ª gen.
Ahora deberías tener una función de 2ª gen. que, cuando se implemente, se comportará de forma idéntica a una instalación nueva de tu extensión. El último paso es verificar y corregir cualquier problema que se haya introducido accidentalmente en el camino.
Asegúrate de usar
firebase-tools
>= 15.24.0 y de implementar tu función de 2ª gen. convertida en un proyecto de prueba
con los recursos adecuados para probar su comportamiento. Si ya tienes un proyecto de prueba configurado para probar tu extensión, usa el comando:
firebase deploy --only functions
Después de ingresar este comando, completa el asistente resultante que te solicita valores de parámetros de la misma manera en que habrías completado el formulario de instalación en la Firebase console para la extensión.
Ejemplo práctico: Stream Firestore to BigQuery
Verificamos la sincronización de Cloud Firestore a BigQuery de extremo a extremo:
- En la consola de Cloud Firestore, crea la colección que estableciste como COLLECTION_PATH (usuarios) si aún no existe.
- Crea un documento llamado bigquery-mirror-test que contenga campos con todos los valores que quieras.
- En la consola de BigQuery, consulta la tabla de registro de cambios sin procesar. Debe contener una sola fila que registre la creación del documento:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
- Consulta la vista más reciente, que debería mostrar el evento de cambio más reciente de
el único documento presente:
bigquery-mirror-test
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
- Borra el documento
bigquery-mirror-testen Cloud Firestore. Desaparece de la vista más reciente y se agrega un eventoDELETEa la tabla de registro de cambios sin procesar.
Puedes inspeccionar el historial completo de un solo documento con lo siguiente:
SELECT *
FROM `PROJECT_ID.analytics.users_raw_changelog`
WHERE document_name = "bigquery-mirror-test"
ORDER BY timestamp ASC
Diferencias con respecto a la prueba de la extensión:
- El activador se implementa como
fsexportbigquery(con prefijo de base de código opcional), no comoext-<instanceId>-fsexportbigquery. Busca ese nombre en el Cloud Functions panel y los registros. - Tu código ahora se ejecutará en el Firebase Local Emulator Suite como funciones normales. Puedes configurar el valor de los parámetros para usar en el emulador con
.env.local. También puedes probar unidades de tu código con el SDK de firebase-functions-test como se describe en Pruebas de unidades de Cloud Functions - El aprovisionamiento ya no está controlado por el entorno de ejecución de Extensiones. Si falta la tabla de registro de cambios
después de la implementación, vuelve a ejecutar la tarea de configuración de forma manual:
firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. La tarea es idempotente, por lo que volver a ejecutarla concilia el conjunto de datos, la tabla y las vistas. - Los valores de los parámetros provienen de
.enven lugar del formulario de instalación, por lo que las nuevas ejecuciones defirebase deployno son interactivas una vez que se completa.env.