diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index 6c68bdeb24c..c786a22c907 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -1,10 +1,9 @@ import * as backend from "./backend"; import * as proto from "../../gcp/proto"; -import * as api from "../../.../../api"; +import * as api from "../../api"; import * as params from "./params"; import { FirebaseError } from "../../error"; import { assertExhaustive, mapObject, nullsafeVisitor } from "../../functional"; -import { UserEnvsOpts, writeUserEnvs } from "../../functions/env"; import { FirebaseConfig } from "./args"; import { Runtime } from "./runtimes/supported"; import { ExprParseError } from "./cel"; @@ -284,22 +283,19 @@ export type DynamicExtension = { interface ResolveBackendOpts { build: Build; firebaseConfig: FirebaseConfig; - userEnvOpt: UserEnvsOpts; userEnvs: Record; nonInteractive?: boolean; isEmulator?: boolean; } /** - * Resolves user-defined parameters inside a Build, and generates a Backend. - * Returns both the Backend and the literal resolved values of any params, since - * the latter also have to be uploaded so user code can see them in process.env + * Resolves user-defined parameters inside a Build and generates a Backend. + * Callers are responsible for persisting resolved env vars. */ export async function resolveBackend( opts: ResolveBackendOpts, ): Promise<{ backend: backend.Backend; envs: Record }> { - let paramValues: Record = {}; - paramValues = await params.resolveParams( + const paramValues = await params.resolveParams( opts.build.params, opts.firebaseConfig, envWithTypes(opts.build.params, opts.userEnvs), @@ -307,16 +303,6 @@ export async function resolveBackend( opts.isEmulator, ); - const toWrite: Record = {}; - for (const paramName of Object.keys(paramValues)) { - const paramValue = paramValues[paramName]; - if (Object.prototype.hasOwnProperty.call(opts.userEnvs, paramName) || paramValue.internal) { - continue; - } - toWrite[paramName] = paramValue.toString(); - } - writeUserEnvs(toWrite, opts.userEnvOpt); - return { backend: toBackend(opts.build, paramValues), envs: paramValues }; } diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index a4c80ca26d4..ff0efbd5f2d 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -135,12 +135,13 @@ export async function prepare( const { backend: wantBackend, envs: resolvedEnvs } = await build.resolveBackend({ build: wantBuild, firebaseConfig, - userEnvOpt, userEnvs, nonInteractive: options.nonInteractive, isEmulator: false, }); + functionsEnv.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt); + let hasEnvsFromParams = false; wantBackend.environmentVariables = envs; for (const envName of Object.keys(resolvedEnvs)) { diff --git a/src/deploy/functions/runtimes/discovery/index.ts b/src/deploy/functions/runtimes/discovery/index.ts index a09fc38db1d..eac941dee12 100644 --- a/src/deploy/functions/runtimes/discovery/index.ts +++ b/src/deploy/functions/runtimes/discovery/index.ts @@ -5,7 +5,7 @@ import * as yaml from "yaml"; import { ChildProcess } from "child_process"; import { logger } from "../../../../logger"; -import * as api from "../../.../../../../api"; +import * as api from "../../../../api"; import * as build from "../../build"; import { Runtime } from "../supported"; import * as v1alpha1 from "./v1alpha1"; diff --git a/src/deploy/functions/runtimes/node/parseTriggers.spec.ts b/src/deploy/functions/runtimes/node/parseTriggers.spec.ts index 37331061c21..caae7f89eaa 100644 --- a/src/deploy/functions/runtimes/node/parseTriggers.spec.ts +++ b/src/deploy/functions/runtimes/node/parseTriggers.spec.ts @@ -17,7 +17,6 @@ async function resolveBackend(bd: build.Build): Promise { storageBucket: "foo.appspot.com", databaseURL: "https://foo.firebaseio.com", }, - userEnvOpt: { functionsSource: "", projectId: "PROJECT" }, userEnvs: {}, }) ).backend; diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index f0e50a3abaa..88055cb30f3 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -580,11 +580,12 @@ export class FunctionsEmulator implements EmulatorInstance { const resolution = await resolveBackend({ build: discoveredBuild, firebaseConfig: JSON.parse(firebaseConfig), - userEnvOpt, userEnvs, nonInteractive: false, isEmulator: true, }); + + functionsEnv.writeResolvedParams(resolution.envs, userEnvs, userEnvOpt); const discoveredBackend = resolution.backend; const endpoints = backend.allEndpoints(discoveredBackend); prepareEndpoints(endpoints); diff --git a/src/functions/env.spec.ts b/src/functions/env.spec.ts index 9f3af8fe7a0..957bea92bd7 100644 --- a/src/functions/env.spec.ts +++ b/src/functions/env.spec.ts @@ -6,6 +6,7 @@ import { expect } from "chai"; import * as env from "./env"; import { FirebaseError } from "../error"; +import { ParamValue } from "../deploy/functions/params"; describe("functions/env", () => { describe("parse", () => { @@ -758,4 +759,87 @@ FOO=foo expect(env.parseStrict(input)).to.deep.equal(expected); }); }); + + describe("writeResolvedParams", () => { + let tmpdir: string; + + beforeEach(() => { + tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "firebase-functions-test-")); + }); + + afterEach(() => { + rmSync(tmpdir, { recursive: true, force: true }); + }); + + it("should write only new, non-internal params", () => { + const resolvedEnvs = { + EXISTING_PARAM: new ParamValue("existing", false, { string: true }), + NEW_PARAM: new ParamValue("new_value", false, { string: true }), + INTERNAL_PARAM: new ParamValue("internal", true, { string: true }), + }; + const userEnvs = { EXISTING_PARAM: "old_value" }; + const userEnvOpt = { + projectId: "test-project", + functionsSource: tmpdir, + }; + + env.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt); + + const writtenContent = fs.readFileSync(path.join(tmpdir, ".env.test-project"), "utf-8"); + expect(writtenContent).to.include("NEW_PARAM=new_value"); + expect(writtenContent).not.to.include("EXISTING_PARAM"); + expect(writtenContent).not.to.include("INTERNAL_PARAM"); + }); + + it("should not create file when no params to write", () => { + const resolvedEnvs = { + EXISTING_PARAM: new ParamValue("existing", false, { string: true }), + INTERNAL_PARAM: new ParamValue("internal", true, { string: true }), + }; + const userEnvs = { EXISTING_PARAM: "old_value" }; + const userEnvOpt = { + projectId: "test-project", + functionsSource: tmpdir, + }; + + env.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt); + + const envFile = path.join(tmpdir, ".env.test-project"); + expect(fs.existsSync(envFile)).to.be.false; + }); + + it("should write to .env.local for emulator", () => { + const resolvedEnvs = { + NEW_PARAM: new ParamValue("emulator_value", false, { string: true }), + }; + const userEnvs = {}; + const userEnvOpt = { + projectId: "test-project", + functionsSource: tmpdir, + isEmulator: true, + }; + + env.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt); + + const writtenContent = fs.readFileSync(path.join(tmpdir, ".env.local"), "utf-8"); + expect(writtenContent).to.include("NEW_PARAM=emulator_value"); + expect(fs.existsSync(path.join(tmpdir, ".env.test-project"))).to.be.false; + }); + + it("should handle params with special characters in values", () => { + const resolvedEnvs = { + NEW_PARAM: new ParamValue("value with\nnewline", false, { string: true }), + }; + const userEnvs = {}; + const userEnvOpt = { + projectId: "test-project", + functionsSource: tmpdir, + }; + + env.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt); + + const writtenContent = fs.readFileSync(path.join(tmpdir, ".env.test-project"), "utf-8"); + expect(writtenContent).to.include('NEW_PARAM="value with\\nnewline"'); + }); + }); }); diff --git a/src/functions/env.ts b/src/functions/env.ts index c6554faabc2..12fc0f4129c 100644 --- a/src/functions/env.ts +++ b/src/functions/env.ts @@ -1,6 +1,7 @@ import * as clc from "colorette"; import * as fs from "fs"; import * as path from "path"; +import { ParamValue } from "../deploy/functions/params"; import { FirebaseError } from "../error"; import { logger } from "../logger"; @@ -414,3 +415,24 @@ export function loadFirebaseEnvs( GCLOUD_PROJECT: projectId, }; } + +/** + * Writes newly resolved params to the appropriate .env file. + * Skips internal params and params that already exist in userEnvs. + */ +export function writeResolvedParams( + resolvedEnvs: Readonly>, + userEnvs: Readonly>, + userEnvOpt: UserEnvsOpts, +): void { + const toWrite: Record = {}; + + for (const paramName of Object.keys(resolvedEnvs)) { + const paramValue = resolvedEnvs[paramName]; + if (!paramValue.internal && !Object.prototype.hasOwnProperty.call(userEnvs, paramName)) { + toWrite[paramName] = paramValue.toString(); + } + } + + writeUserEnvs(toWrite, userEnvOpt); +}