Skip to content

Commit c6b9463

Browse files
committed
fix: rename v1 Event to LegacyEvent to avoid api-extractor conflict
1 parent 7a97e7c commit c6b9463

File tree

8 files changed

+287
-25
lines changed

8 files changed

+287
-25
lines changed

ESM_BUGBASH.md

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Firebase Functions ESM Support Bug Bash
2+
3+
**Author:** Daniel Lee
4+
**Created:** October 28, 2025
5+
**Updated:** October 28, 2025
6+
**Related:** CF3 Bugbashes
7+
8+
## Background
9+
10+
We are shipping native ESM support for `firebase-functions`. This allows developers to use modern JavaScript `import`/`export` syntax directly without transpilation, aligning with the broader Node.js ecosystem. We are using a dual-build approach to maintain backward compatibility for CommonJS users.
11+
12+
## Setup
13+
14+
- Install the pre-release version of the Firebase CLI: `npm i -g firebase-tools@latest` (or specific version if applicable)
15+
- Install the pre-release version of the SDK in your test project: `npm i firebase-functions@next`
16+
17+
## Scenarios to Test
18+
19+
### 1. The "Modern" User (Pure ESM)
20+
21+
Create a fresh project with `"type": "module"`.
22+
23+
**`package.json`**
24+
25+
```json
26+
{
27+
"name": "esm-functions-test",
28+
"type": "module",
29+
"engines": { "node": "18" },
30+
"main": "index.js",
31+
"dependencies": {
32+
"firebase-functions": "next",
33+
"firebase-admin": "latest"
34+
}
35+
}
36+
```
37+
38+
**`index.js`**
39+
40+
```javascript
41+
import { initializeApp } from "firebase-admin/app";
42+
import { onRequest } from "firebase-functions/v2/https";
43+
import { onDocumentCreated } from "firebase-functions/v2/firestore";
44+
import { onSchedule } from "firebase-functions/v2/scheduler";
45+
import * as logger from "firebase-functions/logger";
46+
import { defineString } from "firebase-functions/params";
47+
48+
// V1 imports for comparison/legacy support test
49+
import * as functionsV1 from "firebase-functions/v1";
50+
51+
initializeApp();
52+
53+
const welcomeMessage = defineString("WELCOME_MESSAGE", { default: "Hello from ESM!" });
54+
55+
// V2 HTTP Request
56+
export const helloWorld = onRequest((request, response) => {
57+
logger.info("Hello logs!", { structuredData: true });
58+
response.send(`${welcomeMessage.value()} - V2 HTTP`);
59+
});
60+
61+
// V2 Firestore Trigger
62+
export const onUserCreated = onDocumentCreated("users/{userId}", (event) => {
63+
const snapshot = event.data;
64+
if (!snapshot) {
65+
logger.error("No data associated with the event");
66+
return;
67+
}
68+
const data = snapshot.data();
69+
logger.log("New user created in Firestore (ESM):", data);
70+
return snapshot.ref.set({ signedUpAt: new Date().toISOString() }, { merge: true });
71+
});
72+
73+
// V2 Scheduler
74+
export const everyMinute = onSchedule("every 1 minutes", async (event) => {
75+
logger.debug("Running scheduled task (ESM)");
76+
});
77+
78+
// V1 Auth Trigger (Legacy compatibility in ESM)
79+
export const onAuthUserCreatedV1 = functionsV1.auth.user().onCreate((user) => {
80+
logger.info("V1 Auth trigger running in ESM mode for user:", user.uid);
81+
return null;
82+
});
83+
```
84+
85+
### 2. The "Legacy" User (CommonJS)
86+
87+
Upgrade an existing CJS project.
88+
89+
**`package.json`**
90+
91+
```json
92+
{
93+
"name": "cjs-functions-test",
94+
"engines": { "node": "18" },
95+
"main": "index.js",
96+
"dependencies": {
97+
"firebase-functions": "next",
98+
"firebase-admin": "latest"
99+
}
100+
}
101+
```
102+
103+
**`index.js`**
104+
105+
```javascript
106+
const { initializeApp } = require("firebase-admin/app");
107+
const { onRequest } = require("firebase-functions/v2/https");
108+
const { onDocumentWritten } = require("firebase-functions/v2/firestore");
109+
const logger = require("firebase-functions/logger");
110+
const functionsV1 = require("firebase-functions/v1");
111+
112+
initializeApp();
113+
114+
// V2 HTTP
115+
exports.helloCJS = onRequest((req, res) => {
116+
logger.info("Logging from CJS", { foo: "bar" });
117+
res.send("Hello from CommonJS!");
118+
});
119+
120+
// V2 Firestore
121+
exports.onDataChanged = onDocumentWritten("data/{docId}", (event) => {
122+
logger.warn("Data changed (CJS trigger)");
123+
});
124+
125+
// V1 Realtime Database (Legacy test)
126+
exports.onRTDBWriteV1 = functionsV1.database
127+
.ref("/messages/{pushId}")
128+
.onWrite((change, context) => {
129+
logger.info("V1 RTDB trigger in CJS");
130+
return null;
131+
});
132+
```
133+
134+
### 3. The "Hybrid" User (TypeScript with `NodeNext`)
135+
136+
Test modern TypeScript compilation.
137+
138+
**`package.json`**
139+
140+
```json
141+
{
142+
"name": "ts-nodenext-test",
143+
"type": "module",
144+
"engines": { "node": "18" },
145+
"main": "lib/index.js",
146+
"scripts": {
147+
"build": "tsc",
148+
"serve": "npm run build && firebase emulators:start --only functions",
149+
"deploy": "npm run build && firebase deploy --only functions"
150+
},
151+
"dependencies": {
152+
"firebase-functions": "next",
153+
"firebase-admin": "latest"
154+
},
155+
"devDependencies": {
156+
"typescript": "latest",
157+
"@types/node": "latest"
158+
}
159+
}
160+
```
161+
162+
**`tsconfig.json`**
163+
164+
```json
165+
{
166+
"compilerOptions": {
167+
"module": "NodeNext",
168+
"moduleResolution": "NodeNext",
169+
"target": "ES2022",
170+
"outDir": "lib",
171+
"rootDir": "src",
172+
"sourceMap": true,
173+
"strict": true
174+
},
175+
"include": ["src/**/*"]
176+
}
177+
```
178+
179+
**`src/index.ts`**
180+
181+
```typescript
182+
import { initializeApp } from "firebase-admin/app";
183+
import { onRequest } from "firebase-functions/v2/https";
184+
import * as logger from "firebase-functions/logger";
185+
import { onMessagePublished } from "firebase-functions/v2/pubsub";
186+
187+
initializeApp();
188+
189+
export const tsHttp = onRequest((req, res) => {
190+
logger.info("TypeScript NodeNext HTTP hit");
191+
res.json({ message: "Hello from TS NodeNext" });
192+
});
193+
194+
export const tsPubSub = onMessagePublished("my-topic", (event) => {
195+
logger.info("Received PubSub message", event.data.message.json);
196+
});
197+
```
198+
199+
### 4. The "Dual Hazard" Stress Test (Advanced)
200+
201+
Attempt to load both CJS and ESM versions in the same runtime to check for state duplication.
202+
203+
**`package.json`**
204+
205+
```json
206+
{
207+
"name": "hazard-test",
208+
"type": "module",
209+
"main": "index.js",
210+
"dependencies": {
211+
"firebase-functions": "next"
212+
}
213+
}
214+
```
215+
216+
**`legacy-require.cjs`**
217+
218+
```javascript
219+
const functionsCJS = require("firebase-functions");
220+
module.exports = functionsCJS;
221+
```
222+
223+
**`index.js`**
224+
225+
```javascript
226+
import { onRequest } from "firebase-functions/v2/https";
227+
import * as logger from "firebase-functions/logger";
228+
import { createRequire } from "module";
229+
import * as functionsESM from "firebase-functions";
230+
231+
const require = createRequire(import.meta.url);
232+
const functionsCJS = require("./legacy-require.cjs");
233+
234+
export const hazardTest = onRequest((req, res) => {
235+
const isSameV1 = functionsESM.default === functionsCJS;
236+
237+
logger.info("Dual Package Hazard Check", {
238+
isSameV1Instance: isSameV1,
239+
});
240+
241+
res.json({
242+
hazardDetected: !isSameV1,
243+
message: "If hazardDetected is true, two copies of the SDK are loaded.",
244+
});
245+
});
246+
```
247+
248+
## Focus Areas for Bugs
249+
250+
- **Type resolution errors:** Ensure `tsc` works cleanly in various configurations.
251+
- **Runtime errors:** Watch for `ERR_REQUIRE_ESM` or similar Node.js module loading errors during deployment or emulation.
252+
- **Inconsistent behavior:** If the dual package hazard is triggered, check if global state (like `config()` or initialized apps) behaves unexpectedly.

cloudbuild.test.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
steps:
2+
- name: 'node:22'
3+
entrypoint: 'bash'
4+
args:
5+
- '-c'
6+
- |
7+
node --version
8+
npm --version
9+
npm ci
10+
npm run build

src/v1/cloud-functions.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const WILDCARD_REGEX = new RegExp("{[^/{}]*}", "g");
5454
/**
5555
* Wire format for an event.
5656
*/
57-
export interface Event {
57+
export interface LegacyEvent {
5858
/**
5959
* Wire format for an event context.
6060
*/
@@ -344,7 +344,7 @@ export interface BlockingFunction {
344344
* from your JavaScript file to define a Cloud Function.
345345
*
346346
* This type is a special JavaScript function which takes a templated
347-
* `Event` object as its only argument.
347+
* `LegacyEvent` object as its only argument.
348348
*/
349349
export interface CloudFunction<T> extends Runnable<T> {
350350
(input: any, context?: any): PromiseLike<any> | any;
@@ -361,10 +361,10 @@ export interface CloudFunction<T> extends Runnable<T> {
361361

362362
/** @internal */
363363
export interface MakeCloudFunctionArgs<EventData> {
364-
after?: (raw: Event) => void;
365-
before?: (raw: Event) => void;
364+
after?: (raw: LegacyEvent) => void;
365+
before?: (raw: LegacyEvent) => void;
366366
contextOnlyHandler?: (context: EventContext) => PromiseLike<any> | any;
367-
dataConstructor?: (raw: Event) => EventData;
367+
dataConstructor?: (raw: LegacyEvent) => EventData;
368368
eventType: string;
369369
handler?: (data: EventData, context: EventContext) => PromiseLike<any> | any;
370370
labels?: Record<string, string>;
@@ -382,7 +382,7 @@ export interface MakeCloudFunctionArgs<EventData> {
382382
/** @internal */
383383
export function makeCloudFunction<EventData>({
384384
contextOnlyHandler,
385-
dataConstructor = (raw: Event) => raw.data,
385+
dataConstructor = (raw: LegacyEvent) => raw.data,
386386
eventType,
387387
handler,
388388
labels = {},
@@ -406,7 +406,7 @@ export function makeCloudFunction<EventData>({
406406
};
407407
}
408408

409-
const event: Event = {
409+
const event: LegacyEvent = {
410410
data,
411411
context,
412412
};
@@ -550,7 +550,7 @@ function _makeParams(
550550
return params;
551551
}
552552

553-
function _makeAuth(event: Event, authType: string) {
553+
function _makeAuth(event: LegacyEvent, authType: string) {
554554
if (authType === "UNAUTHENTICATED") {
555555
return null;
556556
}
@@ -560,7 +560,7 @@ function _makeAuth(event: Event, authType: string) {
560560
};
561561
}
562562

563-
function _detectAuthType(event: Event) {
563+
function _detectAuthType(event: LegacyEvent) {
564564
if (event.context?.auth?.admin) {
565565
return "ADMIN";
566566
}

src/v1/providers/analytics.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
// SOFTWARE.
2222

23-
import { CloudFunction, Event, EventContext, makeCloudFunction } from "../cloud-functions";
23+
import { CloudFunction, LegacyEvent, EventContext, makeCloudFunction } from "../cloud-functions";
2424
import { DeploymentOptions } from "../function-configuration";
2525

2626
/** @internal */
@@ -70,7 +70,7 @@ export class AnalyticsEventBuilder {
7070
onLog(
7171
handler: (event: AnalyticsEvent, context: EventContext) => PromiseLike<any> | any
7272
): CloudFunction<AnalyticsEvent> {
73-
const dataConstructor = (raw: Event) => {
73+
const dataConstructor = (raw: LegacyEvent) => {
7474
return new AnalyticsEvent(raw.data);
7575
};
7676
return makeCloudFunction({

src/v1/providers/auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
import {
4141
BlockingFunction,
4242
CloudFunction,
43-
Event,
43+
LegacyEvent,
4444
EventContext,
4545
makeCloudFunction,
4646
optionsToEndpoint,
@@ -108,7 +108,7 @@ export function _userWithOptions(options: DeploymentOptions, userOptions: UserOp
108108
* @public
109109
*/
110110
export class UserBuilder {
111-
private static dataConstructor(raw: Event): UserRecord {
111+
private static dataConstructor(raw: LegacyEvent): UserRecord {
112112
return userRecordConstructor(raw.data);
113113
}
114114

0 commit comments

Comments
 (0)