How to conditionally modify exports from an ES6 Javascript module

I’m wondering if it’s possible to somehow still access the module exports of an ES6 module from within the module, like you do in CommonJS with module.exports.

For clarity, I have a js module (Config.js) that I use to export all of my config variables like so.

export const DatabaseName = "myDbName";
export const DatabasePort = 3000;
export const DatabaseHosts = ["174.292.292.32"];
export const MaxWebRequest = 50;
export const MaxImageRequests = 50;
export const WebRequestTimeout = 30;
etc...

And then I have a separate Dev.Config.js file which only holds the overrides for my dev environment.

export const DatabaseHosts = ["localhost"];
export const DatabasePort = 5500;

In my main Config.js file, I have this logic at the bottom.

try {
    var environmentConfig = `./${process.env.NODE_ENV}.Config.js`;
    var localConfig = require(environmentConfig)
    module.exports = Object.assign(module.exports, localConfig)
} catch (error) {
    console.log("Error overriding config with local values. " + error)
}

And then finally, in my consuming code, I’m able to just import my config.js file like so

import * as Config from "./Config.js";

console.log(Config.DatabaseHosts) // Gives me the correct "overridden" value on my dev environment

Currently I’ve been using babel to transpile my code all back into CommonJS which I guess is how I’m able to sort of mix and match import/export syntax, and still reference module.exports like I’ve done above.

My question is, how would I replicate this pattern in a pure ES6 module without needing to transpile this using babel where I cannot modify my module.exports from within the module itself?

  • Is it important to not use an existing configuration solution?

    – 

  • Such as? It seemed like such a trivial bit of code that I didn’t consider using some other configuration solution. But if there’s a commonly used option out there, I’m open to it. Still curious though from a purely academic standpoint how the above pattern might be replicated in pure ES6.

    – 




  • Just use dynamic imports and top level await if your node version supports that. The rest are hacks. Export a promise from your main config module. Trust me, we have the same name 😉

    – 




  • @Morgan Recommendations are OT but the canonical one is dotenv. There are others (dotenv is ok, a cpl others are similar-but-different and worth looking at).

    – 

Conditional exports isn’t a supported pattern in ESM.

In order to modify exported values using dynamic import from another module whose specifier is derived from an environment variable (in a try...catch statement so that a failed attempt won’t throw an uncaught exception at the top level), you can modify the structure of your exports so that they are exposed as properties on an object. Below is a reproducible example to demonstrate:

./package.json:

{
  "name": "so-77465699",
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "NODE_ENV=Dev node main.js",
    "prod": "NODE_ENV=production node main.js"
  },
  "license": "MIT"
}

./Dev.Config.js:

export const Config = {
  DatabaseHosts: ["localhost"],
  DatabasePort: 5500,
};

export default Config;

./Config.js:

const Config = {
  DatabaseName: "myDbName",
  DatabasePort: 3000,
  DatabaseHosts: ["174.292.292.32"],
  MaxWebRequest: 50,
  MaxImageRequests: 50,
  WebRequestTimeout: 30,
};

try {
  // Import a module specifier based on
  // the value of the NODE_ENV environemnt variable.
  // Destructure and rename the default export:
  const { default: envConfig } = await import(
    import.meta.resolve(`./${process.env.NODE_ENV}.Config.js`)
  );

  // Iterate the keys and values, updating the existing Config object:
  for (const [key, value] of Object.entries(envConfig)) {
    Config[key] = value;
  }
} catch (cause) {
  console.log(`Error overriding config with local values: ${cause}`);
}

export { Config, Config as default };

./main.js:

// Import named export:
import { Config } from "./Config.js";

// Alternatively, since it's also the default export:
// import { default as Config } from "./Config.js";

// Or, using syntax sugar:
// import Config from "./Config.js";

console.log(Config.DatabaseHosts);

In the terminal:

% node --version
v20.9.0

% npm run dev

> [email protected] dev
> NODE_ENV=Dev node main.js

[ 'localhost' ]

% npm run prod

> [email protected] prod
> NODE_ENV=production node main.js

Error overriding config with local values: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/node/so-77465699/production.Config.js' imported from /Users/node/so-77465699/Config.js
[ '174.292.292.32' ]

There is no way to build/overwrite the exports object dynamically in ES6 modules. Even the conditional import will already require either top-level await or tooling support.

What you can do however is mess with let declarations and eval:

export const DatabaseName = "myDbName"; // keep things as `const` to prevent overriding them
export let DatabasePort = 3000;
export let DatabaseHosts = ["174.292.292.32"];
export let MaxWebRequest = 50;
export let MaxImageRequests = 50;
export const WebRequestTimeout = 30;
… // etc

try {
    var environmentConfig = `./${process.env.NODE_ENV}.Config.js`;
    var localConfig = await import(environmentConfig)
    for (const [name, value] of Object.entries(localConfig)) {
        eval(`${name} = value;`);
    }
} catch (error) {
    console.log("Error overriding config with local values. " + error)
}

I would not recommend this though. Use this approach only if you cannot change how the Config.js module is used, or if there are too many exported variables for the alternatives to be feasible.

Instead, I would suggest you create a separate module that merges the configurations, though this requires spelling out the configuration names twice:

// Default.Config.js
export const DatabaseName = "myDbName";
export const DatabasePort = 3000;
export const DatabaseHosts = ["174.292.292.32"];
export const MaxWebRequest = 50;
export const MaxImageRequests = 50;
export const WebRequestTimeout = 30;
// Dev.Config.js
export const DatabaseHosts = ["localhost"];
export const DatabasePort = 5500;
// Config.js
import * as defaultConfig from "./Default.Config.js";

const localConfig = await import(`./${process.env.NODE_ENV}.Config.js`).catch(error => {
    console.log("Error overriding config with local values. " + error);
    return {};
});

export const {
    DatabaseName,
    DatabasePort,
    DatabaseHosts,
    MaxWebRequest,
    MaxImageRequests,
    WebRequestTimeout,
    … // etc
} = Object.assign({}, defaultConfig, localConfig);

Or change the configuration modules to default-export objects, which you can manipulate arbitrarily. You loose the ability to use named imports and get static validation though.

Leave a Comment