From f3018c2487a0de7c04e4ecf5344ec354e955d60f Mon Sep 17 00:00:00 2001 From: kyungseopk1m Date: Thu, 23 Apr 2026 15:27:33 +0900 Subject: [PATCH] fix(functions/emulator): apply hidden-path watcher filter relative to functionsDir chokidar's ignored matcher was evaluating the hidden-segment regex against the absolute file path. When the project was located under a dot-prefixed ancestor directory (e.g. `.worktrees/`, `.cache/`), every file change was silently filtered out and the emulator never reloaded triggers. Match against the path relative to `backend.functionsDir` instead so that the hidden-segment rule only hides dot-prefixed entries inside the watched directory. The other built-in ignore patterns and user-supplied `backend.ignore` globs are unchanged. Fixes #10187 --- CHANGELOG.md | 1 + src/emulator/functionsEmulator.ts | 8 +++- src/emulator/functionsEmulatorShared.spec.ts | 44 ++++++++++++++++++++ src/emulator/functionsEmulatorShared.ts | 21 ++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..83afc91df0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Fixed Functions emulator file watcher silently ignoring changes when the project path contains a dot-prefixed ancestor directory (e.g. `.worktrees/`). (#10187) diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 23fe6167a64..affa5fd16e8 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -35,6 +35,7 @@ import { prepareEndpoints, BlockingTrigger, getTemporarySocketPath, + shouldIgnoreWatchPath, } from "./functionsEmulatorShared"; import { EmulatorRegistry } from "./registry"; import { EmulatorLogger, Verbosity } from "./emulatorLogger"; @@ -519,7 +520,12 @@ export class FunctionsEmulator implements EmulatorInstance { } else { const watcher = chokidar.watch(backend.functionsDir, { ignored: [ - /(^|[\/\\])\../, // Ignore hidden files/dirs (covers .dart_tool, .git, etc.) + // Ignore hidden files/dirs within the watched directory (covers + // .dart_tool, .git, etc.). Match against the path relative to + // functionsDir so a dot-prefixed ancestor directory (e.g. + // `.worktrees/`) does not cause every file change to be ignored. + // See https://github.com/firebase/firebase-tools/issues/10187. + (filePath: string) => shouldIgnoreWatchPath(backend.functionsDir, filePath), /.+\.log/, // Ignore log files /.+?[\\\/]node_modules[\\\/].+?/, // Ignore node_modules /.+?[\\\/]venv[\\\/].+?/, // Ignore venv diff --git a/src/emulator/functionsEmulatorShared.spec.ts b/src/emulator/functionsEmulatorShared.spec.ts index c4ecaec9c80..6af5549d003 100644 --- a/src/emulator/functionsEmulatorShared.spec.ts +++ b/src/emulator/functionsEmulatorShared.spec.ts @@ -1,4 +1,5 @@ import { expect } from "chai"; +import * as path from "path"; import { BackendInfo, EmulatableBackend } from "./functionsEmulator"; import * as functionsEmulatorShared from "./functionsEmulatorShared"; import { @@ -295,4 +296,47 @@ describe("FunctionsEmulatorShared", () => { }); } }); + + describe(`${functionsEmulatorShared.shouldIgnoreWatchPath.name}`, () => { + const functionsDir = path.join("/", "home", "user", ".worktrees", "project", "functions"); + + it("ignores hidden files inside the functions directory", () => { + const target = path.join(functionsDir, ".git", "HEAD"); + expect(functionsEmulatorShared.shouldIgnoreWatchPath(functionsDir, target)).to.be.true; + }); + + it("ignores a hidden segment nested within the functions directory", () => { + const target = path.join(functionsDir, "src", ".cache", "module.js"); + expect(functionsEmulatorShared.shouldIgnoreWatchPath(functionsDir, target)).to.be.true; + }); + + it("does not ignore a regular source file even when the functions directory is nested under a dot-prefixed ancestor", () => { + const target = path.join(functionsDir, "src", "index.ts"); + expect(functionsEmulatorShared.shouldIgnoreWatchPath(functionsDir, target)).to.be.false; + }); + + it("does not ignore a nested source file without any hidden segment", () => { + const target = path.join(functionsDir, "lib", "handlers", "index.js"); + expect(functionsEmulatorShared.shouldIgnoreWatchPath(functionsDir, target)).to.be.false; + }); + + it("does not ignore the functions directory itself", () => { + expect(functionsEmulatorShared.shouldIgnoreWatchPath(functionsDir, functionsDir)).to.be.false; + }); + + describe("when the functions directory has no dot-prefixed ancestor", () => { + const plainFunctionsDir = path.join("/", "home", "user", "project", "functions"); + + it("still ignores hidden files inside the functions directory", () => { + const target = path.join(plainFunctionsDir, ".git", "HEAD"); + expect(functionsEmulatorShared.shouldIgnoreWatchPath(plainFunctionsDir, target)).to.be.true; + }); + + it("does not ignore a regular source file", () => { + const target = path.join(plainFunctionsDir, "src", "index.ts"); + expect(functionsEmulatorShared.shouldIgnoreWatchPath(plainFunctionsDir, target)).to.be + .false; + }); + }); + }); }); diff --git a/src/emulator/functionsEmulatorShared.ts b/src/emulator/functionsEmulatorShared.ts index 852b8d23857..36fdf649722 100644 --- a/src/emulator/functionsEmulatorShared.ts +++ b/src/emulator/functionsEmulatorShared.ts @@ -545,3 +545,24 @@ export function toBackendInfo( }), ); } + +// Matches paths whose last segment starts with a dot (e.g. ".git", ".cache"). +const HIDDEN_SEGMENT_REGEX = /(^|[\/\\])\../; + +/** + * Whether the functions emulator file watcher should ignore `filePath`. + * + * The hidden-segment check is applied against the path relative to + * `functionsDir`. chokidar invokes the ignore matcher with the absolute path, + * so matching the pattern against that path would also match dot-prefixed + * ancestor directories (e.g. `.worktrees/`, `.cache/`) in the containing + * project path and cause every file change to be ignored. See + * https://github.com/firebase/firebase-tools/issues/10187. + */ +export function shouldIgnoreWatchPath(functionsDir: string, filePath: string): boolean { + const rel = path.relative(functionsDir, filePath); + if (!rel) { + return false; + } + return HIDDEN_SEGMENT_REGEX.test(rel); +}