From 5ba92c03fff98d31aaaeb203b778b7a242c480bd Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Thu, 30 Apr 2026 09:50:01 -0400 Subject: [PATCH 1/6] make local builds respect "ignore" files when uploading the built output to GCS for zip deploys --- src/deploy/apphosting/util.spec.ts | 83 ++++++++++++++++++++++++++++++ src/deploy/apphosting/util.ts | 15 ++++-- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/deploy/apphosting/util.spec.ts b/src/deploy/apphosting/util.spec.ts index 9ade32530bd..00f7060c226 100644 --- a/src/deploy/apphosting/util.spec.ts +++ b/src/deploy/apphosting/util.spec.ts @@ -78,5 +78,88 @@ describe("util", () => { expect(files).to.include("dist/index.js"); expect(files).to.not.include("apphosting.yaml"); }); + + it("should respect ignore patterns in config", async () => { + fs.writeFileSync(path.join(distDir, "index.js"), "console.log('hello')"); + fs.writeFileSync(path.join(distDir, "ignored.txt"), "ignore me"); + + const config = { + backendId: "test-backend", + rootDir: "", + ignore: ["**/ignored.txt"], + }; + + const tarballPath: string = await util.createLocalBuildTarArchive( + config, + rootDir, + path.relative(rootDir, distDir), + ); + + const files: string[] = []; + tar.list({ + file: tarballPath, + sync: true, + onentry: (entry: { path: string }) => files.push(entry.path), + }); + + expect(files).to.include("dist/index.js"); + expect(files).to.not.include("dist/ignored.txt"); + }); + + it("should use default ignores when config.ignore is missing", async () => { + fs.writeFileSync(path.join(distDir, "index.js"), "console.log('hello')"); + const nodeModulesDir = path.join(distDir, "node_modules"); + fs.mkdirSync(nodeModulesDir); + fs.writeFileSync(path.join(nodeModulesDir, "some-file.js"), "console.log('vendor')"); + + const configWithoutIgnore = { + backendId: "test-backend", + rootDir: "", + } as any; + + const tarballPath: string = await util.createLocalBuildTarArchive( + configWithoutIgnore, + rootDir, + path.relative(rootDir, distDir), + ); + + const files: string[] = []; + tar.list({ + file: tarballPath, + sync: true, + onentry: (entry: { path: string }) => files.push(entry.path), + }); + + expect(files).to.include("dist/index.js"); + expect(files).to.not.include("dist/node_modules/some-file.js"); + }); + + it("should respect .gitignore patterns", async () => { + fs.writeFileSync(path.join(distDir, "index.js"), "console.log('hello')"); + fs.writeFileSync(path.join(distDir, "gitignored.txt"), "ignore me"); + fs.writeFileSync(path.join(distDir, ".gitignore"), "gitignored.txt"); + + const config = { + backendId: "test-backend", + rootDir: "", + ignore: [], + }; + + const tarballPath: string = await util.createLocalBuildTarArchive( + config, + rootDir, + path.relative(rootDir, distDir), + ); + + const files: string[] = []; + tar.list({ + file: tarballPath, + sync: true, + onentry: (entry: { path: string }) => files.push(entry.path), + }); + + expect(files).to.include("dist/index.js"); + expect(files).to.not.include("dist/gitignored.txt"); + }); }); }); diff --git a/src/deploy/apphosting/util.ts b/src/deploy/apphosting/util.ts index b96d76e42e2..e80bc6cd7a7 100644 --- a/src/deploy/apphosting/util.ts +++ b/src/deploy/apphosting/util.ts @@ -28,7 +28,7 @@ export async function createLocalBuildTarArchive( const tmpFile = tmp.fileSync({ prefix: `${config.backendId}-`, postfix: ".tar.gz" }).name; const targetDir = targetSubDir ? path.join(rootDir, targetSubDir) : rootDir; - const ignore = ["firebase-debug.log", "firebase-debug.*.log", ".git"]; + const ignore = resolveIgnorePatterns(config, targetDir); const rdrFiles = await fsAsync.readdirRecursive({ path: targetDir, ignore: ignore, @@ -90,10 +90,7 @@ export async function createSourceDeployArchive( const targetDir = targetSubDir ? path.join(rootDir, targetSubDir) : rootDir; // We must ignore firebase-debug.log or weird things happen if you're in the public dir when you deploy. - const ignore = config.ignore || ["node_modules", ".git"]; - ignore.push("firebase-debug.log", "firebase-debug.*.log"); - const gitIgnorePatterns = parseGitIgnorePatterns(targetDir); - ignore.push(...gitIgnorePatterns); + const ignore = resolveIgnorePatterns(config, targetDir); try { const files = await fsAsync.readdirRecursive({ path: targetDir, @@ -117,6 +114,14 @@ export async function createSourceDeployArchive( return tmpFile; } +export function resolveIgnorePatterns(config: AppHostingSingle, targetDir: string): string[] { + const ignore = config.ignore ? [...config.ignore] : ["node_modules", ".git"]; + ignore.push("firebase-debug.log", "firebase-debug.*.log"); + const gitIgnorePatterns = parseGitIgnorePatterns(targetDir); + ignore.push(...gitIgnorePatterns); + return ignore; +} + function parseGitIgnorePatterns(projectRoot: string, gitIgnorePath = ".gitignore"): string[] { const absoluteFilePath = path.resolve(projectRoot, gitIgnorePath); if (!fs.existsSync(absoluteFilePath)) { From c5a9f74e4625482b887c7f2fad6cd4545e36bbaa Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Thu, 30 Apr 2026 19:46:38 -0400 Subject: [PATCH 2/6] Make it so we copy local build files into a separate temporary folder before we actually build it --- local_build | 1 + src/deploy/apphosting/prepare.ts | 51 +++++++++++++++++++++++++++++++- src/deploy/apphosting/util.ts | 4 +-- 3 files changed, 53 insertions(+), 3 deletions(-) create mode 160000 local_build diff --git a/local_build b/local_build new file mode 160000 index 00000000000..5ba92c03fff --- /dev/null +++ b/local_build @@ -0,0 +1 @@ +Subproject commit 5ba92c03fff98d31aaaeb203b778b7a242c480bd diff --git a/src/deploy/apphosting/prepare.ts b/src/deploy/apphosting/prepare.ts index 2b312ca22cd..574c8e20089 100644 --- a/src/deploy/apphosting/prepare.ts +++ b/src/deploy/apphosting/prepare.ts @@ -1,4 +1,7 @@ +import * as fs from "fs"; import * as path from "path"; +import * as fsAsync from "../../fsAsync"; +import { resolveIgnorePatterns } from "./util"; import { doSetupSourceDeploy, ensureAppHostingComputeServiceAccount, @@ -187,10 +190,15 @@ export default async function (context: Context, options: Options): Promise { + // Resolve ignores for local builds, skipping default node_modules ignore + const ignore = resolveIgnorePatterns(cfg, rootDir, /* skipDefaultNodeModules= */ true); + ignore.push("local_build"); // Always ignore the build directory itself + + // Warn if node_modules is explicitly ignored + if (cfg.ignore?.includes("node_modules")) { + logLabeledWarning( + "apphosting", + `You have included 'node_modules' in your ignore list for local builds. This might cause the build to fail if dependencies are missing in the build directory.`, + ); + } + + // Create local_build dir + if (fs.existsSync(localBuildDir)) { + fs.rmSync(localBuildDir, { recursive: true, force: true }); + } + fs.mkdirSync(localBuildDir, { recursive: true }); + + // Copy files respecting ignores + const filesToCopy = await fsAsync.readdirRecursive({ + path: rootDir, + ignore: ignore, + isGitIgnore: true, + }); + + for (const file of filesToCopy) { + const relativePath = path.relative(rootDir, file.name); + const destPath = path.join(localBuildDir, relativePath); + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + fs.copyFileSync(file.name, destPath); + } +} diff --git a/src/deploy/apphosting/util.ts b/src/deploy/apphosting/util.ts index e80bc6cd7a7..f4fa00aeff9 100644 --- a/src/deploy/apphosting/util.ts +++ b/src/deploy/apphosting/util.ts @@ -114,8 +114,8 @@ export async function createSourceDeployArchive( return tmpFile; } -export function resolveIgnorePatterns(config: AppHostingSingle, targetDir: string): string[] { - const ignore = config.ignore ? [...config.ignore] : ["node_modules", ".git"]; +export function resolveIgnorePatterns(config: AppHostingSingle, targetDir: string, skipDefaultNodeModules = false): string[] { + const ignore = config.ignore ? [...config.ignore] : (skipDefaultNodeModules ? [".git"] : ["node_modules", ".git"]); ignore.push("firebase-debug.log", "firebase-debug.*.log"); const gitIgnorePatterns = parseGitIgnorePatterns(targetDir); ignore.push(...gitIgnorePatterns); From 703e26fdd75d161bfffba49b566dfbdd8b9650e4 Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Thu, 30 Apr 2026 20:22:27 -0400 Subject: [PATCH 3/6] fix the test cleanup --- src/deploy/apphosting/prepare.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/deploy/apphosting/prepare.spec.ts b/src/deploy/apphosting/prepare.spec.ts index f4614408b22..53e88b792db 100644 --- a/src/deploy/apphosting/prepare.spec.ts +++ b/src/deploy/apphosting/prepare.spec.ts @@ -24,6 +24,8 @@ import * as apphostingUtils from "../../apphosting/utils"; import { AppHostingYamlConfig, EnvMap } from "../../apphosting/yaml"; import { Options } from "../../options"; import { AppHostingSingle } from "../../firebaseConfig"; +import * as fs from "fs"; +import * as fsAsync from "../../fsAsync"; const BASE_OPTS = { cwd: "/", @@ -88,6 +90,12 @@ describe("apphosting", () => { addServiceAccountToRolesStub = sinon .stub(resourceManager, "addServiceAccountToRoles") .resolves(); + + sinon.stub(fs, "existsSync").returns(false); + sinon.stub(fs, "mkdirSync").returns(undefined as any); + sinon.stub(fs, "rmSync").returns(undefined); + sinon.stub(fs, "copyFileSync").returns(undefined); + sinon.stub(fsAsync, "readdirRecursive").resolves([]); }); afterEach(() => { From 3afd1699523860532793a0061e4e470f6e5415c1 Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Thu, 30 Apr 2026 20:23:20 -0400 Subject: [PATCH 4/6] delete local_build folder --- local_build | 1 - 1 file changed, 1 deletion(-) delete mode 160000 local_build diff --git a/local_build b/local_build deleted file mode 160000 index 5ba92c03fff..00000000000 --- a/local_build +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5ba92c03fff98d31aaaeb203b778b7a242c480bd From 62edfd720d9df40d149bcd697057f411d6ce3b9a Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Thu, 30 Apr 2026 20:24:37 -0400 Subject: [PATCH 5/6] run the linters/formatters again --- src/deploy/apphosting/util.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/deploy/apphosting/util.ts b/src/deploy/apphosting/util.ts index f4fa00aeff9..16761a3cb8f 100644 --- a/src/deploy/apphosting/util.ts +++ b/src/deploy/apphosting/util.ts @@ -114,8 +114,16 @@ export async function createSourceDeployArchive( return tmpFile; } -export function resolveIgnorePatterns(config: AppHostingSingle, targetDir: string, skipDefaultNodeModules = false): string[] { - const ignore = config.ignore ? [...config.ignore] : (skipDefaultNodeModules ? [".git"] : ["node_modules", ".git"]); +export function resolveIgnorePatterns( + config: AppHostingSingle, + targetDir: string, + skipDefaultNodeModules = false, +): string[] { + const ignore = config.ignore + ? [...config.ignore] + : skipDefaultNodeModules + ? [".git"] + : ["node_modules", ".git"]; ignore.push("firebase-debug.log", "firebase-debug.*.log"); const gitIgnorePatterns = parseGitIgnorePatterns(targetDir); ignore.push(...gitIgnorePatterns); From afaac8f1290ea4289c4185b3fad5bd4f61693669 Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Thu, 30 Apr 2026 20:27:34 -0400 Subject: [PATCH 6/6] test and formatting fixes --- src/deploy/apphosting/prepare.spec.ts | 2 +- src/deploy/apphosting/util.spec.ts | 7 +++++-- src/deploy/apphosting/util.ts | 6 +++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/deploy/apphosting/prepare.spec.ts b/src/deploy/apphosting/prepare.spec.ts index 53e88b792db..af8252e6226 100644 --- a/src/deploy/apphosting/prepare.spec.ts +++ b/src/deploy/apphosting/prepare.spec.ts @@ -92,7 +92,7 @@ describe("apphosting", () => { .resolves(); sinon.stub(fs, "existsSync").returns(false); - sinon.stub(fs, "mkdirSync").returns(undefined as any); + sinon.stub(fs, "mkdirSync").returns(undefined); sinon.stub(fs, "rmSync").returns(undefined); sinon.stub(fs, "copyFileSync").returns(undefined); sinon.stub(fsAsync, "readdirRecursive").resolves([]); diff --git a/src/deploy/apphosting/util.spec.ts b/src/deploy/apphosting/util.spec.ts index 00f7060c226..ae773c49c7e 100644 --- a/src/deploy/apphosting/util.spec.ts +++ b/src/deploy/apphosting/util.spec.ts @@ -4,6 +4,7 @@ import * as path from "path"; import * as tmp from "tmp"; import * as tar from "tar"; import * as util from "./util"; +import { AppHostingSingle } from "../../firebaseConfig"; describe("util", () => { let tmpDir: tmp.DirResult; @@ -112,12 +113,14 @@ describe("util", () => { fs.mkdirSync(nodeModulesDir); fs.writeFileSync(path.join(nodeModulesDir, "some-file.js"), "console.log('vendor')"); - const configWithoutIgnore = { + const configWithoutIgnore: AppHostingSingle = { backendId: "test-backend", rootDir: "", - } as any; + ignore: undefined as unknown as string[], + }; const tarballPath: string = await util.createLocalBuildTarArchive( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument configWithoutIgnore, rootDir, path.relative(rootDir, distDir), diff --git a/src/deploy/apphosting/util.ts b/src/deploy/apphosting/util.ts index 16761a3cb8f..c06d2044225 100644 --- a/src/deploy/apphosting/util.ts +++ b/src/deploy/apphosting/util.ts @@ -107,13 +107,17 @@ export async function createSourceDeployArchive( await pipeAsync(archive, fileStream); } catch (err: unknown) { throw new FirebaseError( - `Could not read source directory. Remove links and shortcuts and try again. Original: ${err}`, + `Could not read source directory. Remove links and shortcuts and try again. Original: ${String(err)}`, { original: err as Error, exit: 1 }, ); } return tmpFile; } +/** + * Resolves the ignore patterns for App Hosting deployments. + * Merges config ignores, defaults, and .gitignore patterns. + */ export function resolveIgnorePatterns( config: AppHostingSingle, targetDir: string,