From eb85ac08fc28a47c1ef4378aed394cceab3e030d Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 3 Dec 2025 21:17:35 -0800 Subject: [PATCH 1/5] fix: Resolve dual-package hazard for params in ESM We resolve two critical issues caused by the "Dual-Package Hazard" when using `firebase-functions` in ESM projects: 1. Separate instances of the `declaredParams` in `firebase-functions/params` led to params not being detected when used in ESM modules. To fix, we implement a Global Singleton pattern for `declaredParams` using `globalThis` and a version-scoped `Symbol.for` key. This ensures both builds share the same global storage for collecting param API uses. 1. `instanceof` checks failed because the CJS `Expression`/`ResetValue` instances deferred from the ESM ones. To fix, we implement a custom `[Symbol.hasInstance]` on `Expression` and `ResetValue` classes using specfic branded tags to identify instances across package boundaries. --- .../bin-test/sources/commonjs-params/index.js | 8 +++ .../sources/commonjs-params/package.json | 4 ++ scripts/bin-test/sources/esm-params/index.js | 8 +++ .../bin-test/sources/esm-params/package.json | 4 ++ scripts/bin-test/test.ts | 52 +++++++++++++++++++ src/common/options.ts | 16 ++++++ src/params/index.ts | 22 +++++++- src/params/types.ts | 17 ++++++ tsdown.config.mts | 10 ++++ 9 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 scripts/bin-test/sources/commonjs-params/index.js create mode 100644 scripts/bin-test/sources/commonjs-params/package.json create mode 100644 scripts/bin-test/sources/esm-params/index.js create mode 100644 scripts/bin-test/sources/esm-params/package.json diff --git a/scripts/bin-test/sources/commonjs-params/index.js b/scripts/bin-test/sources/commonjs-params/index.js new file mode 100644 index 000000000..3dac9f734 --- /dev/null +++ b/scripts/bin-test/sources/commonjs-params/index.js @@ -0,0 +1,8 @@ +const { defineInt } = require("firebase-functions/params"); +const { onRequest } = require("firebase-functions/v2/https"); + +const minInstances = defineInt("MIN_INSTANCES", { default: 1 }); + +exports.v2http = onRequest({ minInstances }, (req, res) => { + res.send("PASS"); +}); diff --git a/scripts/bin-test/sources/commonjs-params/package.json b/scripts/bin-test/sources/commonjs-params/package.json new file mode 100644 index 000000000..1bfd80923 --- /dev/null +++ b/scripts/bin-test/sources/commonjs-params/package.json @@ -0,0 +1,4 @@ +{ + "name": "commonjs-params", + "main": "index.js" +} diff --git a/scripts/bin-test/sources/esm-params/index.js b/scripts/bin-test/sources/esm-params/index.js new file mode 100644 index 000000000..2055b63a4 --- /dev/null +++ b/scripts/bin-test/sources/esm-params/index.js @@ -0,0 +1,8 @@ +import { defineInt } from "firebase-functions/params"; +import { onRequest } from "firebase-functions/v2/https"; + +const minInstances = defineInt("MIN_INSTANCES", { default: 1 }); + +export const v2http = onRequest({ minInstances }, (req, res) => { + res.send("PASS"); +}); diff --git a/scripts/bin-test/sources/esm-params/package.json b/scripts/bin-test/sources/esm-params/package.json new file mode 100644 index 000000000..2a9ed5519 --- /dev/null +++ b/scripts/bin-test/sources/esm-params/package.json @@ -0,0 +1,4 @@ +{ + "name": "esm-params", + "type": "module" +} diff --git a/scripts/bin-test/test.ts b/scripts/bin-test/test.ts index d24eec5cd..c22d6b00b 100644 --- a/scripts/bin-test/test.ts +++ b/scripts/bin-test/test.ts @@ -362,6 +362,32 @@ describe("functions.yaml", function () { extensions: BASE_EXTENSIONS, }, }, + { + name: "with params", + modulePath: "./scripts/bin-test/sources/commonjs-params", + expected: { + endpoints: { + v2http: { + ...DEFAULT_V2_OPTIONS, + platform: "gcfv2", + entryPoint: "v2http", + labels: {}, + httpsTrigger: {}, + minInstances: "{{ params.MIN_INSTANCES }}", + }, + }, + requiredAPIs: [], + specVersion: "v1alpha1", + params: [ + { + name: "MIN_INSTANCES", + type: "int", + default: 1, + }, + ], + extensions: {}, + }, + }, ]; for (const tc of testcases) { @@ -396,6 +422,32 @@ describe("functions.yaml", function () { modulePath: "./scripts/bin-test/sources/esm-ext", expected: BASE_STACK, }, + { + name: "with params", + modulePath: "./scripts/bin-test/sources/esm-params", + expected: { + endpoints: { + v2http: { + ...DEFAULT_V2_OPTIONS, + platform: "gcfv2", + entryPoint: "v2http", + labels: {}, + httpsTrigger: {}, + minInstances: "{{ params.MIN_INSTANCES }}", + }, + }, + requiredAPIs: [], + specVersion: "v1alpha1", + params: [ + { + name: "MIN_INSTANCES", + type: "int", + default: 1, + }, + ], + extensions: {}, + }, + }, ]; for (const tc of testcases) { diff --git a/src/common/options.ts b/src/common/options.ts index 229fc1f27..88153a723 100644 --- a/src/common/options.ts +++ b/src/common/options.ts @@ -19,12 +19,28 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +const RESET_VALUE_TAG = Symbol.for("firebase-functions:ResetValue:Tag"); + /** * Special configuration type to reset configuration to platform default. * * @alpha */ export class ResetValue { + /** + * Handle the "Dual-Package Hazard" where the CLI (CJS) loads the CJS build + * but user code (ESM) loads the ESM build. + * + * We implement `Symbol.hasInstance` to allow the CLI to recognize ResetValue + * instances from the ESM build by checking for the global symbol tag. + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return (instance as { [RESET_VALUE_TAG]?: boolean })?.[RESET_VALUE_TAG] === true; + } + + get [RESET_VALUE_TAG](): boolean { + return true; + } toJSON(): null { return null; } diff --git a/src/params/index.ts b/src/params/index.ts index cde9ecf3c..d9e32ffe9 100644 --- a/src/params/index.ts +++ b/src/params/index.ts @@ -46,7 +46,27 @@ export { Expression }; export type { ParamOptions }; type SecretOrExpr = Param | SecretParam | JsonSecretParam; -export const declaredParams: SecretOrExpr[] = []; + +/** + * Use a global singleton to manage the list of declared parameters. + * This ensures that parameters are shared between CJS and ESM builds, + * avoiding the "dual-package hazard" where the CLI (CJS) sees an empty list + * while the user's code (ESM) populates a different list. + */ +const majorVersion = + // @ts-expect-error __FIREBASE_FUNCTIONS_MAJOR_VERSION__ is injected at build time + typeof __FIREBASE_FUNCTIONS_MAJOR_VERSION__ !== "undefined" + ? // @ts-expect-error __FIREBASE_FUNCTIONS_MAJOR_VERSION__ is injected at build time + __FIREBASE_FUNCTIONS_MAJOR_VERSION__ + : "0"; +const GLOBAL_SYMBOL = Symbol.for(`firebase-functions:params:declaredParams:v${majorVersion}`); +const globalSymbols = globalThis as unknown as Record; + +if (!globalSymbols[GLOBAL_SYMBOL]) { + globalSymbols[GLOBAL_SYMBOL] = []; +} + +export const declaredParams: SecretOrExpr[] = globalSymbols[GLOBAL_SYMBOL]; /** * Use a helper to manage the list such that parameters are uniquely diff --git a/src/params/types.ts b/src/params/types.ts index e937e2e33..974b696aa 100644 --- a/src/params/types.ts +++ b/src/params/types.ts @@ -22,12 +22,29 @@ import * as logger from "../logger"; +const EXPRESSION_TAG = Symbol.for("firebase-functions:Expression:Tag"); + /* * A CEL expression which can be evaluated during function deployment, and * resolved to a value of the generic type parameter: i.e, you can pass * an Expression as the value of an option that normally accepts numbers. */ export abstract class Expression { + /** + * Handle the "Dual-Package Hazard" where the CLI (CJS) loads the CJS build + * but user code (ESM) loads the ESM build. In this case, the class instances + * are different, so `instanceof Expression` fails. + * + * We implement `Symbol.hasInstance` to allow the CLI to recognize Expression + * instances from the ESM build by checking for the global symbol tag. + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return (instance as { [EXPRESSION_TAG]?: boolean })?.[EXPRESSION_TAG] === true; + } + + get [EXPRESSION_TAG](): boolean { + return true; + } /** Returns the expression's runtime value, based on the CLI's resolution of parameters. */ value(): T { if (process.env.FUNCTIONS_CONTROL_API === "true") { diff --git a/tsdown.config.mts b/tsdown.config.mts index 3f40420bb..e68ecad13 100644 --- a/tsdown.config.mts +++ b/tsdown.config.mts @@ -1,4 +1,8 @@ import { defineConfig } from "tsdown"; +import { readFileSync } from "fs"; + +const pkg = JSON.parse(readFileSync("./package.json", "utf-8")); +const majorVersion = pkg.version.split(".")[0]; const rewriteProtoPathMjs = { name: "rewrite-proto-path-mjs", @@ -23,6 +27,9 @@ export default defineConfig([ dts: false, // Use tsc for type declarations treeshake: false, external: ["../../../protos/compiledFirestore"], + define: { + __FIREBASE_FUNCTIONS_MAJOR_VERSION__: JSON.stringify(majorVersion), + }, }, { entry: "src/**/*.ts", @@ -33,5 +40,8 @@ export default defineConfig([ dts: false, // Use tsc for type declarations treeshake: false, plugins: [rewriteProtoPathMjs], + define: { + __FIREBASE_FUNCTIONS_MAJOR_VERSION__: JSON.stringify(majorVersion), + }, }, ]); \ No newline at end of file From 7336492c7c1b949d72dbaed87b2f8dab54bc357f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 3 Dec 2025 21:17:35 -0800 Subject: [PATCH 2/5] fix: Resolve dual-package hazard for params in ESM This commit resolves two critical issues caused by the "Dual-Package Hazard" when using `firebase-functions` in ESM projects: 1. **Split State (Missing Params)**: The SDK bootstrap code (CJS) and User Code (ESM) were using separate instances of the `declaredParams` array. * **Fix**: Implemented a Global Singleton pattern for `declaredParams` using `globalThis` and a version-scoped `Symbol.for` key. This ensures both builds share the same storage. 2. **Identity Mismatch (Deployment Error)**: `instanceof Expression` checks in the SDK bootstrap code failed because the user's `Expression` instance (ESM) differed from the bootstrap's class (CJS). * **Fix**: Implemented `[Symbol.hasInstance]` on `Expression` and `ResetValue` classes. * **Mechanism**: Uses `Symbol.for` tags (`firebase-functions:Expression:Tag`) to robustly identify instances across package boundaries, avoiding fragile duck-typing or strict class identity checks. This ensures that parameterized configuration works correctly regardless of the module system used. --- CHANGELOG.md | 2 + .../bin-test/sources/commonjs-params/index.js | 8 +++ .../sources/commonjs-params/package.json | 4 ++ scripts/bin-test/sources/esm-params/index.js | 8 +++ .../bin-test/sources/esm-params/package.json | 4 ++ scripts/bin-test/test.ts | 52 +++++++++++++++++++ src/common/options.ts | 16 ++++++ src/params/index.ts | 22 +++++++- src/params/types.ts | 17 ++++++ tsdown.config.mts | 11 ++++ 10 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 scripts/bin-test/sources/commonjs-params/index.js create mode 100644 scripts/bin-test/sources/commonjs-params/package.json create mode 100644 scripts/bin-test/sources/esm-params/index.js create mode 100644 scripts/bin-test/sources/esm-params/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..7f4b85971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +# Unreleased +- Fix "Dual-Package Hazard" for parameterized configuration in ESM projects. (#1780) diff --git a/scripts/bin-test/sources/commonjs-params/index.js b/scripts/bin-test/sources/commonjs-params/index.js new file mode 100644 index 000000000..3dac9f734 --- /dev/null +++ b/scripts/bin-test/sources/commonjs-params/index.js @@ -0,0 +1,8 @@ +const { defineInt } = require("firebase-functions/params"); +const { onRequest } = require("firebase-functions/v2/https"); + +const minInstances = defineInt("MIN_INSTANCES", { default: 1 }); + +exports.v2http = onRequest({ minInstances }, (req, res) => { + res.send("PASS"); +}); diff --git a/scripts/bin-test/sources/commonjs-params/package.json b/scripts/bin-test/sources/commonjs-params/package.json new file mode 100644 index 000000000..1bfd80923 --- /dev/null +++ b/scripts/bin-test/sources/commonjs-params/package.json @@ -0,0 +1,4 @@ +{ + "name": "commonjs-params", + "main": "index.js" +} diff --git a/scripts/bin-test/sources/esm-params/index.js b/scripts/bin-test/sources/esm-params/index.js new file mode 100644 index 000000000..2055b63a4 --- /dev/null +++ b/scripts/bin-test/sources/esm-params/index.js @@ -0,0 +1,8 @@ +import { defineInt } from "firebase-functions/params"; +import { onRequest } from "firebase-functions/v2/https"; + +const minInstances = defineInt("MIN_INSTANCES", { default: 1 }); + +export const v2http = onRequest({ minInstances }, (req, res) => { + res.send("PASS"); +}); diff --git a/scripts/bin-test/sources/esm-params/package.json b/scripts/bin-test/sources/esm-params/package.json new file mode 100644 index 000000000..2a9ed5519 --- /dev/null +++ b/scripts/bin-test/sources/esm-params/package.json @@ -0,0 +1,4 @@ +{ + "name": "esm-params", + "type": "module" +} diff --git a/scripts/bin-test/test.ts b/scripts/bin-test/test.ts index d24eec5cd..c22d6b00b 100644 --- a/scripts/bin-test/test.ts +++ b/scripts/bin-test/test.ts @@ -362,6 +362,32 @@ describe("functions.yaml", function () { extensions: BASE_EXTENSIONS, }, }, + { + name: "with params", + modulePath: "./scripts/bin-test/sources/commonjs-params", + expected: { + endpoints: { + v2http: { + ...DEFAULT_V2_OPTIONS, + platform: "gcfv2", + entryPoint: "v2http", + labels: {}, + httpsTrigger: {}, + minInstances: "{{ params.MIN_INSTANCES }}", + }, + }, + requiredAPIs: [], + specVersion: "v1alpha1", + params: [ + { + name: "MIN_INSTANCES", + type: "int", + default: 1, + }, + ], + extensions: {}, + }, + }, ]; for (const tc of testcases) { @@ -396,6 +422,32 @@ describe("functions.yaml", function () { modulePath: "./scripts/bin-test/sources/esm-ext", expected: BASE_STACK, }, + { + name: "with params", + modulePath: "./scripts/bin-test/sources/esm-params", + expected: { + endpoints: { + v2http: { + ...DEFAULT_V2_OPTIONS, + platform: "gcfv2", + entryPoint: "v2http", + labels: {}, + httpsTrigger: {}, + minInstances: "{{ params.MIN_INSTANCES }}", + }, + }, + requiredAPIs: [], + specVersion: "v1alpha1", + params: [ + { + name: "MIN_INSTANCES", + type: "int", + default: 1, + }, + ], + extensions: {}, + }, + }, ]; for (const tc of testcases) { diff --git a/src/common/options.ts b/src/common/options.ts index 229fc1f27..88153a723 100644 --- a/src/common/options.ts +++ b/src/common/options.ts @@ -19,12 +19,28 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +const RESET_VALUE_TAG = Symbol.for("firebase-functions:ResetValue:Tag"); + /** * Special configuration type to reset configuration to platform default. * * @alpha */ export class ResetValue { + /** + * Handle the "Dual-Package Hazard" where the CLI (CJS) loads the CJS build + * but user code (ESM) loads the ESM build. + * + * We implement `Symbol.hasInstance` to allow the CLI to recognize ResetValue + * instances from the ESM build by checking for the global symbol tag. + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return (instance as { [RESET_VALUE_TAG]?: boolean })?.[RESET_VALUE_TAG] === true; + } + + get [RESET_VALUE_TAG](): boolean { + return true; + } toJSON(): null { return null; } diff --git a/src/params/index.ts b/src/params/index.ts index cde9ecf3c..d9e32ffe9 100644 --- a/src/params/index.ts +++ b/src/params/index.ts @@ -46,7 +46,27 @@ export { Expression }; export type { ParamOptions }; type SecretOrExpr = Param | SecretParam | JsonSecretParam; -export const declaredParams: SecretOrExpr[] = []; + +/** + * Use a global singleton to manage the list of declared parameters. + * This ensures that parameters are shared between CJS and ESM builds, + * avoiding the "dual-package hazard" where the CLI (CJS) sees an empty list + * while the user's code (ESM) populates a different list. + */ +const majorVersion = + // @ts-expect-error __FIREBASE_FUNCTIONS_MAJOR_VERSION__ is injected at build time + typeof __FIREBASE_FUNCTIONS_MAJOR_VERSION__ !== "undefined" + ? // @ts-expect-error __FIREBASE_FUNCTIONS_MAJOR_VERSION__ is injected at build time + __FIREBASE_FUNCTIONS_MAJOR_VERSION__ + : "0"; +const GLOBAL_SYMBOL = Symbol.for(`firebase-functions:params:declaredParams:v${majorVersion}`); +const globalSymbols = globalThis as unknown as Record; + +if (!globalSymbols[GLOBAL_SYMBOL]) { + globalSymbols[GLOBAL_SYMBOL] = []; +} + +export const declaredParams: SecretOrExpr[] = globalSymbols[GLOBAL_SYMBOL]; /** * Use a helper to manage the list such that parameters are uniquely diff --git a/src/params/types.ts b/src/params/types.ts index e937e2e33..974b696aa 100644 --- a/src/params/types.ts +++ b/src/params/types.ts @@ -22,12 +22,29 @@ import * as logger from "../logger"; +const EXPRESSION_TAG = Symbol.for("firebase-functions:Expression:Tag"); + /* * A CEL expression which can be evaluated during function deployment, and * resolved to a value of the generic type parameter: i.e, you can pass * an Expression as the value of an option that normally accepts numbers. */ export abstract class Expression { + /** + * Handle the "Dual-Package Hazard" where the CLI (CJS) loads the CJS build + * but user code (ESM) loads the ESM build. In this case, the class instances + * are different, so `instanceof Expression` fails. + * + * We implement `Symbol.hasInstance` to allow the CLI to recognize Expression + * instances from the ESM build by checking for the global symbol tag. + */ + static [Symbol.hasInstance](instance: unknown): boolean { + return (instance as { [EXPRESSION_TAG]?: boolean })?.[EXPRESSION_TAG] === true; + } + + get [EXPRESSION_TAG](): boolean { + return true; + } /** Returns the expression's runtime value, based on the CLI's resolution of parameters. */ value(): T { if (process.env.FUNCTIONS_CONTROL_API === "true") { diff --git a/tsdown.config.mts b/tsdown.config.mts index 3f40420bb..248f9bff4 100644 --- a/tsdown.config.mts +++ b/tsdown.config.mts @@ -1,4 +1,8 @@ import { defineConfig } from "tsdown"; +import { readFileSync } from "fs"; + +const pkg = JSON.parse(readFileSync("./package.json", "utf-8")); +const majorVersion = pkg.version.split(".")[0]; const rewriteProtoPathMjs = { name: "rewrite-proto-path-mjs", @@ -13,6 +17,11 @@ const rewriteProtoPathMjs = { // Note: We use tsc (via tsconfig.release.json) for .d.ts generation instead of tsdown's // built-in dts option due to issues with rolldown-plugin-dts. // See: https://github.com/sxzz/rolldown-plugin-dts/issues/121 + +const define = { + __FIREBASE_FUNCTIONS_MAJOR_VERSION__: JSON.stringify(majorVersion), +}; + export default defineConfig([ { entry: "src/**/*.ts", @@ -23,6 +32,7 @@ export default defineConfig([ dts: false, // Use tsc for type declarations treeshake: false, external: ["../../../protos/compiledFirestore"], + define, }, { entry: "src/**/*.ts", @@ -33,5 +43,6 @@ export default defineConfig([ dts: false, // Use tsc for type declarations treeshake: false, plugins: [rewriteProtoPathMjs], + define, }, ]); \ No newline at end of file From 1f1f754567e7fa5ea2a750fb796feb0c1f113f73 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 3 Dec 2025 21:32:16 -0800 Subject: [PATCH 3/5] fix changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4b85971..fcaa37c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1 @@ -# Unreleased - Fix "Dual-Package Hazard" for parameterized configuration in ESM projects. (#1780) From 6d9abbc9db4a952a19334e0923ee7bb646285390 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 3 Dec 2025 21:36:55 -0800 Subject: [PATCH 4/5] clarify comments. --- src/common/options.ts | 11 +++++------ src/params/index.ts | 5 +++-- src/params/types.ts | 8 +++----- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/common/options.ts b/src/common/options.ts index 88153a723..be6d016c3 100644 --- a/src/common/options.ts +++ b/src/common/options.ts @@ -28,11 +28,10 @@ const RESET_VALUE_TAG = Symbol.for("firebase-functions:ResetValue:Tag"); */ export class ResetValue { /** - * Handle the "Dual-Package Hazard" where the CLI (CJS) loads the CJS build - * but user code (ESM) loads the ESM build. + * Handle the "Dual-Package Hazard". * - * We implement `Symbol.hasInstance` to allow the CLI to recognize ResetValue - * instances from the ESM build by checking for the global symbol tag. + * We implement custom `Symbol.hasInstance` to so CJS/ESM ResetValue instances + * are recognized as the same type. */ static [Symbol.hasInstance](instance: unknown): boolean { return (instance as { [RESET_VALUE_TAG]?: boolean })?.[RESET_VALUE_TAG] === true; @@ -45,7 +44,7 @@ export class ResetValue { return null; } // eslint-disable-next-line @typescript-eslint/no-empty-function - private constructor() {} + private constructor() { } public static getInstance() { return new ResetValue(); } @@ -60,5 +59,5 @@ export const RESET_VALUE = ResetValue.getInstance(); * @internal */ export type ResettableKeys = Required<{ - [K in keyof T as [ResetValue] extends [T[K]] ? K : never]: null; + [K in keyof T as[ResetValue] extends [T[K]] ? K : never]: null; }>; diff --git a/src/params/index.ts b/src/params/index.ts index d9e32ffe9..baef1c760 100644 --- a/src/params/index.ts +++ b/src/params/index.ts @@ -49,9 +49,10 @@ type SecretOrExpr = Param | SecretParam | JsonSecretParam; /** * Use a global singleton to manage the list of declared parameters. + * * This ensures that parameters are shared between CJS and ESM builds, - * avoiding the "dual-package hazard" where the CLI (CJS) sees an empty list - * while the user's code (ESM) populates a different list. + * avoiding the "dual-package hazard" where the src/bin/firebase-functions.ts (CJS) sees + * an empty list while the user's code (ESM) populates a different list. */ const majorVersion = // @ts-expect-error __FIREBASE_FUNCTIONS_MAJOR_VERSION__ is injected at build time diff --git a/src/params/types.ts b/src/params/types.ts index 974b696aa..14f7ce69d 100644 --- a/src/params/types.ts +++ b/src/params/types.ts @@ -31,12 +31,10 @@ const EXPRESSION_TAG = Symbol.for("firebase-functions:Expression:Tag"); */ export abstract class Expression { /** - * Handle the "Dual-Package Hazard" where the CLI (CJS) loads the CJS build - * but user code (ESM) loads the ESM build. In this case, the class instances - * are different, so `instanceof Expression` fails. + * Handle the "Dual-Package Hazard" . * - * We implement `Symbol.hasInstance` to allow the CLI to recognize Expression - * instances from the ESM build by checking for the global symbol tag. + * We implement custom `Symbol.hasInstance` to so CJS/ESM Expression instances + * are recognized as the same type. */ static [Symbol.hasInstance](instance: unknown): boolean { return (instance as { [EXPRESSION_TAG]?: boolean })?.[EXPRESSION_TAG] === true; From fe045b5bbfe29a635dde003b46d56ca064792b18 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 3 Dec 2025 21:38:56 -0800 Subject: [PATCH 5/5] formatter --- src/common/options.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/options.ts b/src/common/options.ts index be6d016c3..6f9fd650c 100644 --- a/src/common/options.ts +++ b/src/common/options.ts @@ -44,7 +44,7 @@ export class ResetValue { return null; } // eslint-disable-next-line @typescript-eslint/no-empty-function - private constructor() { } + private constructor() {} public static getInstance() { return new ResetValue(); } @@ -59,5 +59,5 @@ export const RESET_VALUE = ResetValue.getInstance(); * @internal */ export type ResettableKeys = Required<{ - [K in keyof T as[ResetValue] extends [T[K]] ? K : never]: null; + [K in keyof T as [ResetValue] extends [T[K]] ? K : never]: null; }>;