Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f5da647
fix(emulator): warn instead of exiting when the Firestore rules watch…
gauranshahuja Sep 5, 2026
dd67af9
fix(emulator): guard against non-Error values in the rules watcher er…
gauranshahuja Sep 5, 2026
a167500
fix(emulator): close the rules watcher in afterEach before removing t…
gauranshahuja Sep 9, 2026
9bf0541
Merge branch 'main' into 4298/rules-watcher-fail-gracefully
gauranshahuja Sep 9, 2026
edff5a0
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Sep 11, 2026
b22a884
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Sep 11, 2026
559dd3e
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Sep 12, 2026
b6afd47
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Sep 25, 2026
03945d9
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Sep 26, 2026
7e79e44
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Sep 28, 2026
e8c0ea2
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Sep 28, 2026
f1d0af8
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Sep 28, 2026
2b8e4cf
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Sep 29, 2026
1f31cbf
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Sep 29, 2026
be01362
Merge upstream/main into 4298/rules-watcher-fail-gracefully
gauranshahuja Oct 3, 2026
039e10c
Merge upstream/main into 4298/rules-watcher-fail-gracefully
gauranshahuja Oct 7, 2026
11da836
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Oct 10, 2026
2102d52
Merge remote-tracking branch 'upstream/main' into 4298/rules-watcher-…
gauranshahuja Oct 17, 2026
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
@@ -1 +1,2 @@
- Ensured the Firebase Hosting API is enabled before deploying and running other Hosting commands.
- Fixed the Firestore emulator exiting when it could not watch the rules file for changes. It now logs a warning instead. (#4298)
87 changes: 87 additions & 0 deletions src/emulator/firestoreEmulator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { expect } from "chai";
import { FSWatcher } from "chokidar";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as sinon from "sinon";

import * as downloadableEmulators from "./downloadableEmulators";
import { FirestoreEmulator } from "./firestoreEmulator";
import * as utils from "../utils";

/** Returns the emulator's rules watcher, failing the test if it was not created. */
function rulesWatcherOf(emulator: FirestoreEmulator): FSWatcher {
const watcher = emulator.rulesWatcher;
expect(watcher, "rules watcher should be created").to.not.be.undefined;
return watcher as FSWatcher;
}

describe("FirestoreEmulator", () => {
const sandbox = sinon.createSandbox();
let logLabeledWarningStub: sinon.SinonStub;
let tmpDir: string;
let rulesPath: string;
let emulator: FirestoreEmulator | undefined;

beforeEach(() => {
sandbox.stub(downloadableEmulators, "start").resolves();
sandbox.stub(downloadableEmulators, "stop").resolves();
logLabeledWarningStub = sandbox.stub(utils, "logLabeledWarning");
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "firestore-emulator-spec-"));
rulesPath = path.join(tmpDir, "firestore.rules");
fs.writeFileSync(rulesPath, "rules_version = '2';");
});

afterEach(async () => {
sandbox.restore();
// Close the watcher even if a test failed before reaching stop(), so nothing keeps tmpDir open.
await emulator?.rulesWatcher?.close();
emulator = undefined;
fs.rmSync(tmpDir, { recursive: true, force: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: before rmSync let's do rulesWatcher.close()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a167500: afterEach now awaits rulesWatcher.close() before rmSync, so a test that fails before reaching stop() no longer leaves a watcher open on the temp dir. The four specs, eslint and tsc are clean locally.

});

it("does not watch a rules file when none is configured", async () => {
emulator = new FirestoreEmulator({ project_id: "demo-test" });
await emulator.start();

expect(emulator.rulesWatcher).to.be.undefined;
await emulator.stop();
});

it("logs a warning instead of exiting when the rules watcher fails", async () => {
emulator = new FirestoreEmulator({ project_id: "demo-test", rules: rulesPath });
await emulator.start();
const watcher = rulesWatcherOf(emulator);

// Without an "error" listener, EventEmitter#emit throws, which is what terminated the CLI (#4298).
const err = new Error("ENOSPC: System limit for number of file watchers reached");
expect(() => watcher.emit("error", err)).to.not.throw();

expect(logLabeledWarningStub).to.have.been.calledOnce;
const [label, message] = logLabeledWarningStub.firstCall.args as [string, string];
expect(label).to.equal("firestore");
expect(message).to.include(rulesPath).and.to.include("ENOSPC");
await emulator.stop();
});

it("handles a non-Error value emitted by the rules watcher", async () => {
emulator = new FirestoreEmulator({ project_id: "demo-test", rules: rulesPath });
await emulator.start();
const watcher = rulesWatcherOf(emulator);
expect(() => watcher.emit("error", "watcher exploded")).to.not.throw();
expect(logLabeledWarningStub).to.have.been.calledOnce;
const [, message] = logLabeledWarningStub.firstCall.args as [string, string];
expect(message).to.include("watcher exploded");
await emulator.stop();
});

it("closes the rules watcher on stop", async () => {
emulator = new FirestoreEmulator({ project_id: "demo-test", rules: rulesPath });
await emulator.start();
const closeSpy = sandbox.spy(rulesWatcherOf(emulator), "close");

await emulator.stop();

expect(closeSpy).to.have.been.calledOnce;
});
});
10 changes: 10 additions & 0 deletions src/emulator/firestoreEmulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ export class FirestoreEmulator implements EmulatorInstance {
if (this.args.rules && this.args.project_id) {
const rulesPath = this.args.rules;
this.rulesWatcher = chokidar.watch(rulesPath, { persistent: true, ignoreInitial: true });
// Without an "error" listener, a watcher failure (e.g. no inotify support in some CI or
// Docker environments) is thrown from the EventEmitter and terminates the whole CLI.
this.rulesWatcher.on("error", (err: unknown) => {
const message = err instanceof Error ? err.message : String(err);
utils.logLabeledWarning(
"firestore",
`Could not watch ${rulesPath} for changes, so rules hot reloading is disabled: ${message}. ` +
"If your environment does not support file watching, set CHOKIDAR_USEPOLLING=1 to fall back to polling.",
);
});
this.rulesWatcher.on("change", async () => {
// There have been some race conditions reported (on Windows) where reading the
// file too quickly after the watcher fires results in an empty file being read.
Expand Down
Loading