Repository: firebase/firebase-tools (ts)

Issue:
Able to specify custom Firebase.json file path in cli
It would be great to have the ability to specify a firebase.json path in the CLI, and with this have the option to have multiple firebase config files.

example usecase:

I want to separate my functions into production and dev groups, since I have a couple of functions that are dev only for now. Since deploy is handled by a CI and I have hosting rewrite rules in my firebase.json, I would specify a production firebase json and a dev firebase json with their appropriate rules

```
exports.productionGroup = {
  function1: functions.exampleFunction1
};

exports.devGroup = {
  function1: functions.exampleFunction1,
  function2: functions.exampleFunction2
};
```

prod.firebase.json
```
{
  "hosting": {
    "public": "public",
    "rewrites": [
        {
          "source": "/api/exampleFunction1", "function": "productionGroup-exampleFunction1"
        }
      ]
  }
}
```

dev.firebase.json
```
{
  "hosting": {
    "public": "public",
    "rewrites": [
        {
          "source": "/api/exampleFunction1", "function": "devGroup-exampleFunction1"
        },
        {
          "source": "/api/exampleFunction2", "function": "devGroup-exampleFunction2"
        }
      ]
  }
}
```

To deploy:

```
firebase deploy --config dev.firebase.json
```
or
```
firebase deploy --config prod.firebase.json
```

Pull request:
Add --config flag
<!--

Thank you for contributing to the Firebase community! Please fill out the form below.

Run the linter and test suite
==============================
Run `npm test` to make sure your changes compile properly and the tests all pass on your local machine. We've hooked up this repo with continuous integration to double check those things for you.

-->

### Description

Fixes #1115 

### Scenarios Tested

I have this directory structure:
```
$ ls
firebase-alt.json  public/
firebase.json      public-alt/
```

Normal serve:
```
$ firebase serve

=== Serving from '/private/var/folders/xl/6lkrzp7j07581mw8_4dlt3b00
0643s/T/tmp.RAEPRTJr'...

i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000
```

Alt serve:
```
$ firebase --config firebase-alt.json serve

=== Serving from '/private/var/folders/xl/6lkrzp7j07581mw8_4dlt3b00
0643s/T/tmp.RAEPRTJr'...

i  hosting: Serving hosting files from: public-alt
✔  hosting: Local server: http://localhost:5000
```

Normal serve from subdir:
```
$ cd public
$ firebase serve

=== Serving from '/private/var/folders/xl/6lkrzp7j07581mw8_4dlt3b000643s/T/tmp.RAEPRTJr'...

i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000
```

Alt serve from subdir:
```
$ cd public
$ firebase --config=../firebase-alt.json serve

=== Serving from '/private/var/folders/xl/6lkrzp7j07581mw8_4dlt3b000643s/T/tmp.RAEPRTJr'...

i  hosting: Serving hosting files from: public-alt
✔  hosting: Local server: http://localhost:5000
```

Non-existent file:
```
$ firebase --config=banana.json serve

Error: Could not load config file banana.json.
```

### Sample Commands

Adding `--config`/`-c` to all commands.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c02cd93a6..6ce4d28ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,2 +1,3 @@
 - Fixes issue where `Authorization` header was missing from callable functions in the emulator (#2459).
 - Improve support for the Node.js 12 (Beta) runtime in the Functions emulator.
+- Allow specifying the config (`firebase.json`) file using the `--config`/`-c` flag.
diff --git a/src/command.ts b/src/command.ts
index 87b206880..9ce6a2e0a 100644
--- a/src/command.ts
+++ b/src/command.ts
@@ -227,15 +227,21 @@ export class Command {
       setupLoggers();
     }
 
+    if (getInheritedOption(options, "config")) {
+      options.configPath = getInheritedOption(options, "config");
+    }
+
     try {
       options.config = _load(options);
     } catch (e) {
       options.configError = e;
     }
 
-    options.projectRoot = detectProjectRoot(options.cwd);
+    options.projectRoot = detectProjectRoot(options);
     this.applyRC(options);
-    if (options.project) validateProjectId(options.project);
+    if (options.project) {
+      validateProjectId(options.project);
+    }
   }
 
   /**
@@ -244,7 +250,7 @@ export class Command {
    */
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   private applyRC(options: any): void {
-    const rc = load(options.cwd);
+    const rc = load(options);
     options.rc = rc;
 
     options.project =
diff --git a/src/config.js b/src/config.js
index 3f0118a83..4a4c80951 100644
--- a/src/config.js
+++ b/src/config.js
@@ -17,7 +17,7 @@ var utils = require("./utils");
 
 var Config = function(src, options) {
   this.options = options || {};
-  this.projectDir = options.projectDir || detectProjectRoot(options.cwd);
+  this.projectDir = options.projectDir || detectProjectRoot(options);
 
   this._src = src;
   this.data = {};
@@ -97,7 +97,7 @@ Config.prototype._materialize = function(target) {
 };
 
 Config.prototype._parseFile = function(target, filePath) {
-  var fullPath = resolveProjectPath(this.options.cwd, filePath);
+  var fullPath = resolveProjectPath(this.options, filePath);
   var ext = path.extname(filePath);
   if (!fsutils.fileExistsSync(fullPath)) {
     throw new FirebaseError("Parse Error: Imported file " + filePath + " does not exist", {
@@ -208,13 +208,15 @@ Config.prototype.askWriteProjectFile = function(p, content) {
 };
 
 Config.load = function(options, allowMissing) {
-  var pd = detectProjectRoot(options.cwd);
+  const pd = detectProjectRoot(options);
+  const filename = options.configPath || Config.FILENAME;
   if (pd) {
     try {
-      var data = cjson.load(path.join(pd, Config.FILENAME));
+      const filePath = path.resolve(pd, path.basename(filename));
+      const data = cjson.load(filePath);
       return new Config(data, options);
     } catch (e) {
-      throw new FirebaseError("There was an error loading firebase.json:\n\n" + e.message, {
+      throw new FirebaseError(`There was an error loading ${filename}:\n\n` + e.message, {
         exit: 1,
       });
     }
diff --git a/src/deploy/functions/prepare.js b/src/deploy/functions/prepare.js
index 26ddaa189..70ffe36e0 100644
--- a/src/deploy/functions/prepare.js
+++ b/src/deploy/functions/prepare.js
@@ -22,7 +22,7 @@ module.exports = function(context, options, payload) {
   var runtimeFromConfig = options.config.get("functions.runtime");
 
   try {
-    validator.functionsDirectoryExists(options.cwd, sourceDirName);
+    validator.functionsDirectoryExists(options, sourceDirName);
     validator.functionNamesAreValid(functionNames);
     // it's annoying that we have to pass in both sourceDirName and sourceDir
     // but they are two different methods on the config object, so cannot get
diff --git a/src/deploy/functions/validate.ts b/src/deploy/functions/validate.ts
index 7c05fd7fb..9fc7c20e8 100644
--- a/src/deploy/functions/validate.ts
+++ b/src/deploy/functions/validate.ts
@@ -13,12 +13,12 @@ const cjson = require("cjson");
 
 /**
  * Check that functions directory exists.
- * @param cwd Working directory.
+ * @param options options object.
  * @param sourceDirName Relative path to source directory.
  * @throws { FirebaseError } Functions directory must exist.
  */
-export function functionsDirectoryExists(cwd: string, sourceDirName: string): void {
-  if (!fsutils.dirExistsSync(projectPath.resolveProjectPath(cwd, sourceDirName))) {
+export function functionsDirectoryExists(options: object, sourceDirName: string): void {
+  if (!fsutils.dirExistsSync(projectPath.resolveProjectPath(options, sourceDirName))) {
     const msg =
       `could not deploy functions because the ${clc.bold('"' + sourceDirName + '"')} ` +
       `directory was not found. Please create it or specify a different source directory in firebase.json`;
diff --git a/src/deploy/hosting/prepare.js b/src/deploy/hosting/prepare.js
index 73e03af22..b1e9c96fa 100644
--- a/src/deploy/hosting/prepare.js
+++ b/src/deploy/hosting/prepare.js
@@ -59,7 +59,7 @@ module.exports = function(context, options) {
       );
     }
 
-    if (!fsutils.dirExistsSync(resolveProjectPath(options.cwd, cfg.public))) {
+    if (!fsutils.dirExistsSync(resolveProjectPath(options, cfg.public))) {
       throw new FirebaseError(
         `Specified public directory '${cfg.public}' does not exist, ` +
           `can't deploy hosting to site ${deploy.site}`,
diff --git a/src/deploy/hosting/uploader.js b/src/deploy/hosting/uploader.js
index 003c9e123..b5587e9d6 100644
--- a/src/deploy/hosting/uploader.js
+++ b/src/deploy/hosting/uploader.js
@@ -28,7 +28,7 @@ class Uploader {
     this.version = options.version;
     this.cwd = options.cwd || process.cwd();
 
-    this.projectRoot = detectProjectRoot(this.cwd);
+    this.projectRoot = detectProjectRoot(options);
 
     this.gzipLevel = options.gzipLevel || 9;
     this.hashQueue = new Queue({
diff --git a/src/detectProjectRoot.ts b/src/detectProjectRoot.ts
index 1c8e82611..de3991169 100644
--- a/src/detectProjectRoot.ts
+++ b/src/detectProjectRoot.ts
@@ -1,8 +1,18 @@
 import { fileExistsSync } from "./fsutils";
+import { FirebaseError } from "./error";
 import { dirname, resolve } from "path";
 
-export function detectProjectRoot(cwd: string): string | null {
-  let projectRootDir = cwd || process.cwd();
+export function detectProjectRoot(options: { cwd?: string; configPath?: string }): string | null {
+  let projectRootDir = options.cwd || process.cwd();
+  if (options.configPath) {
+    const fullPath = resolve(projectRootDir, options.configPath);
+    if (!fileExistsSync(fullPath)) {
+      throw new FirebaseError(`Could not load config file ${options.configPath}.`, { exit: 1 });
+    }
+
+    return dirname(fullPath);
+  }
+
   while (!fileExistsSync(resolve(projectRootDir, "./firebase.json"))) {
     const parentDir = dirname(projectRootDir);
     if (parentDir === projectRootDir) {
diff --git a/src/index.js b/src/index.js
index 8e0094f74..55e4ba15b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -17,6 +17,7 @@ program.option("--token <token>", "supply an auth token for this command");
 program.option("--non-interactive", "error out of the command instead of waiting for prompts");
 program.option("-i, --interactive", "force prompts to be displayed");
 program.option("--debug", "print verbose debug output and keep a debug log file");
+program.option("-c, --config <path>", "path to the firebase.json file to use for configuration");
 
 var client = {};
 client.cli = program;
diff --git a/src/projectPath.ts b/src/projectPath.ts
index b359e9258..74f660ea0 100644
--- a/src/projectPath.ts
+++ b/src/projectPath.ts
@@ -4,12 +4,15 @@ import { FirebaseError } from "./error";
 
 /**
  * Returns a fully qualified path to the wanted file/directory inside the project.
- * @param cwd current working directory.
+ * @param options options object.
  * @param filePath the target file or directory in the project.
  * @return the fully resolved path within the project directory
  */
-export function resolveProjectPath(cwd: string, filePath: string): string {
-  const projectRoot = detectProjectRoot(cwd);
+export function resolveProjectPath(
+  options: { cwd?: string; configPath?: string },
+  filePath: string
+): string {
+  const projectRoot = detectProjectRoot(options);
   if (!projectRoot) {
     throw new FirebaseError("Expected to be in a project directory, but none was found.", {
       exit: 2,
diff --git a/src/rc.js b/src/rc.js
index ea53770e0..8241c9b0d 100644
--- a/src/rc.js
+++ b/src/rc.js
@@ -201,10 +201,10 @@ RC.loadFile = function(rcpath) {
   return new RC(rcpath, data);
 };
 
-RC.load = function(cwd) {
-  cwd = cwd || process.cwd();
-  var dir = detectProjectRoot(cwd);
-  var potential = path.resolve(dir || cwd, "./.firebaserc");
+RC.load = function(options) {
+  const cwd = options.cwd || process.cwd();
+  const dir = detectProjectRoot(options);
+  const potential = path.resolve(dir || cwd, "./.firebaserc");
   return RC.loadFile(potential);
 };
 
diff --git a/src/serve/hosting.ts b/src/serve/hosting.ts
index 000c9580a..def07395d 100644
--- a/src/serve/hosting.ts
+++ b/src/serve/hosting.ts
@@ -48,7 +48,7 @@ function startServer(options: any, config: any, port: number, init: TemplateServ
     port: port,
     host: options.host,
     config: config,
-    cwd: detectProjectRoot(options.cwd),
+    cwd: detectProjectRoot(options),
     stack: "strict",
     before: {
       files: (req: Request, res: Response, next: NextFunction) => {


Test patch:
diff --git a/src/test/config.spec.js b/src/test/config.spec.js
index ce7a9911f..9333dd87b 100644
--- a/src/test/config.spec.js
+++ b/src/test/config.spec.js
@@ -11,6 +11,16 @@ var _fixtureDir = function(name) {
 };
 
 describe("Config", function() {
+  describe("#load", () => {
+    it("should load a cjson file when configPath is specified", () => {
+      const config = Config.load({
+        cwd: __dirname,
+        configPath: "./fixtures/valid-config/firebase.json",
+      });
+      expect(config.get("database.rules")).to.eq("config/security-rules.json");
+    });
+  });
+
   describe("#_parseFile", function() {
     it("should load a cjson file", function() {
       var config = new Config({}, { cwd: _fixtureDir("config-imports") });
diff --git a/src/test/deploy/functions/validate.spec.ts b/src/test/deploy/functions/validate.spec.ts
index 75e1619a2..284882140 100644
--- a/src/test/deploy/functions/validate.spec.ts
+++ b/src/test/deploy/functions/validate.spec.ts
@@ -30,7 +30,7 @@ describe("validate", () => {
       dirExistsStub.returns(true);
 
       expect(() => {
-        validate.functionsDirectoryExists("cwd", "sourceDirName");
+        validate.functionsDirectoryExists({ cwd: "cwd" }, "sourceDirName");
       }).to.not.throw();
     });
 
@@ -39,7 +39,7 @@ describe("validate", () => {
       dirExistsStub.returns(false);
 
       expect(() => {
-        validate.functionsDirectoryExists("cwd", "sourceDirName");
+        validate.functionsDirectoryExists({ cwd: "cwd" }, "sourceDirName");
       }).to.throw(FirebaseError);
     });
   });
diff --git a/src/test/rc.spec.js b/src/test/rc.spec.js
index 8aa6cb183..9d8cfdc09 100644
--- a/src/test/rc.spec.js
+++ b/src/test/rc.spec.js
@@ -11,19 +11,24 @@ var fixturesDir = path.resolve(__dirname, "./fixtures");
 describe("RC", function() {
   describe(".load", function() {
     it("should load from nearest project directory", function() {
-      var result = RC.load(path.resolve(fixturesDir, "fbrc/conflict"));
+      var result = RC.load({ cwd: path.resolve(fixturesDir, "fbrc/conflict") });
       expect(result.projects.default).to.eq("top");
     });
 
     it("should be an empty object when not in project dir", function() {
-      var result = RC.load(__dirname);
+      var result = RC.load({ cwd: __dirname });
       return expect(result.data).to.deep.eq({});
     });
 
     it("should not throw up on invalid json", function() {
-      var result = RC.load(path.resolve(fixturesDir, "fbrc/invalid"));
+      var result = RC.load({ cwd: path.resolve(fixturesDir, "fbrc/invalid") });
       return expect(result.data).to.deep.eq({});
     });
+
+    it("should load from the right directory when --config is specified", () => {
+      const result = RC.load({ cwd: __dirname, configPath: "./fixtures/fbrc/firebase.json" });
+      expect(result.projects.default).to.eq("top");
+    });
   });
 
   describe("instance methods", function() {