Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fix "Dual-Package Hazard" for parameterized configuration in ESM projects. (#1780)
8 changes: 8 additions & 0 deletions scripts/bin-test/sources/commonjs-params/index.js
Original file line number Diff line number Diff line change
@@ -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");
});
4 changes: 4 additions & 0 deletions scripts/bin-test/sources/commonjs-params/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "commonjs-params",
"main": "index.js"
}
8 changes: 8 additions & 0 deletions scripts/bin-test/sources/esm-params/index.js
Original file line number Diff line number Diff line change
@@ -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");
});
4 changes: 4 additions & 0 deletions scripts/bin-test/sources/esm-params/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "esm-params",
"type": "module"
}
52 changes: 52 additions & 0 deletions scripts/bin-test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 15 additions & 0 deletions src/common/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,27 @@
// 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".
*
* 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;
}

get [RESET_VALUE_TAG](): boolean {
return true;
}
toJSON(): null {
return null;
}
Expand Down
23 changes: 22 additions & 1 deletion src/params/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,28 @@ export { Expression };
export type { ParamOptions };

type SecretOrExpr = Param<any> | SecretParam | JsonSecretParam<any>;
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 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
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<symbol, SecretOrExpr[]>;

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
Expand Down
15 changes: 15 additions & 0 deletions src/params/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,27 @@

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<number> as the value of an option that normally accepts numbers.
*/
export abstract class Expression<T extends string | number | boolean | string[]> {
/**
* Handle the "Dual-Package Hazard" .
*
* 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;
}

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") {
Expand Down
11 changes: 11 additions & 0 deletions tsdown.config.mts
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand All @@ -23,6 +32,7 @@ export default defineConfig([
dts: false, // Use tsc for type declarations
treeshake: false,
external: ["../../../protos/compiledFirestore"],
define,
},
{
entry: "src/**/*.ts",
Expand All @@ -33,5 +43,6 @@ export default defineConfig([
dts: false, // Use tsc for type declarations
treeshake: false,
plugins: [rewriteProtoPathMjs],
define,
},
]);
Loading