From 5d0f0ce218e6cb2b52a160116ecd18310375df22 Mon Sep 17 00:00:00 2001 From: Sam Severance Date: Mon, 18 May 2026 13:20:56 -0400 Subject: [PATCH] Add mergeConfig option to opt out of implicit config object merging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a plain object is passed as config via the programmatic API, superstatic merges it with any superstatic.json or firebase.json found on disk. Add mergeConfig: false to use the provided object as-is. This only affects programmatic usage — the CLI always uses the specified config file directly and was never subject to merging. Resolves #503 --- README.md | 3 +- src/loaders/config-file.js | 7 +++-- src/options.ts | 1 + src/superstatic.js | 2 +- test/unit/loaders/config-file.spec.js | 42 +++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 41317d4..c89daed 100755 --- a/README.md +++ b/README.md @@ -203,7 +203,8 @@ Instantiates middleware. See an [example](https://github.com/firebase/superstati * `options` - Optional configuration: * `fallthrough` - When `false`, render a 404 page from within Superstatic rather than calling through to the next middleware. Defaults to `true`. - * `config` - A file path to your application's configuration file (see [Configuration](#configuration)) or an object containing your application's configuration. If an object is provided, it will be merged into existing config in a `superstatic.json`. + * `config` - A file path to your application's configuration file (see [Configuration](#configuration)) or an object containing your application's configuration. If an object is provided, it will be merged into existing config in a `superstatic.json` or `firebase.json` unless `mergeConfig` is set to `false`. + * `mergeConfig` - When `false`, the config object you provide is used as-is without being merged with any `superstatic.json` or `firebase.json` found on disk. Defaults to `true`. Only applies when `config` is a plain object. * `protect` - Adds HTTP basic auth. Example: `username:password` * `env`- A file path your application's environment variables file or an object containing values that are made available at the urls `/__/env.json` and `/__/env.js`. See the documentation detail on [environment variables](http://docs.firebase.com/guides/environment-variables). * `cwd` - The current working directory to set as the root. Your application's `public` configuration option will be used relative to this. diff --git a/src/loaders/config-file.js b/src/loaders/config-file.js index 9b158a8..f6a12ab 100644 --- a/src/loaders/config-file.js +++ b/src/loaders/config-file.js @@ -27,7 +27,7 @@ const { isPlainObject } = require("../utils/objectutils"); const CONFIG_FILE = ["superstatic.json", "firebase.json"]; -module.exports = function (filename) { +module.exports = function (filename, mergeConfig = true) { if (typeof filename === "function") { return filename; } @@ -35,14 +35,17 @@ module.exports = function (filename) { filename = filename ?? CONFIG_FILE; let configObject = {}; + let configObjectProvided = false; let config = {}; // From custom config data passed in try { configObject = JSON.parse(filename); + configObjectProvided = true; } catch { if (isPlainObject(filename)) { configObject = filename; + configObjectProvided = true; filename = CONFIG_FILE; } } @@ -71,5 +74,5 @@ module.exports = function (filename) { // Passing an object as the config value merges // the config data - return { ...config, ...configObject }; + return configObjectProvided && !mergeConfig ? configObject : { ...config, ...configObject }; }; diff --git a/src/options.ts b/src/options.ts index 61f265b..e3402ff 100644 --- a/src/options.ts +++ b/src/options.ts @@ -3,6 +3,7 @@ import { Configuration } from "./config"; export interface MiddlewareOptions { fallthrough?: boolean; + mergeConfig?: boolean; config?: string | Configuration; protect?: string; env?: string | Record; diff --git a/src/superstatic.js b/src/superstatic.js index f18e042..0de4290 100644 --- a/src/superstatic.js +++ b/src/superstatic.js @@ -51,7 +51,7 @@ const superstatic = function (spec = {}) { // Load data /** @type {Configuration} */ - const config = (spec.config = loadConfigFile(spec.config)); + const config = (spec.config = loadConfigFile(spec.config, spec.mergeConfig)); config.errorPage = config.errorPage ?? "/404.html"; // Set up provider diff --git a/test/unit/loaders/config-file.spec.js b/test/unit/loaders/config-file.spec.js index dbba00f..8024cc6 100644 --- a/test/unit/loaders/config-file.spec.js +++ b/test/unit/loaders/config-file.spec.js @@ -77,6 +77,48 @@ describe("loading config files", () => { done(); }); + describe("mergeConfig: false", () => { + it("returns object as-is without merging with superstatic.json", async () => { + await fs.writeFile( + "superstatic.json", + '{"firebase": "superstatic", "public": "./"}', + "utf-8", + ); + + const config = loadConfigFile({ public: "app" }, false); + + expect(config).to.eql({ public: "app" }); + + await fs.rm("superstatic.json"); + }); + + it("returns object as-is without merging with firebase.json", async () => { + await fs.writeFile( + "firebase.json", + '{"firebase": "example", "public": "./"}', + "utf-8", + ); + + const config = loadConfigFile({ public: "app" }, false); + + expect(config).to.eql({ public: "app" }); + + await fs.rm("firebase.json"); + }); + + it("returns file config as-is when mergeConfig is false and config is a file path", async () => { + await fs.writeFile( + ".tmp/myconfig.json", + '{"public": "app"}', + "utf-8", + ); + + const config = loadConfigFile(".tmp/myconfig.json", false); + + expect(config).to.eql({ public: "app" }); + }); + }); + describe("extends the file config with the object passed", () => { it("superstatic.json", async () => { await fs.writeFile(