skip to Main Content

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?

2

Answers


  1. 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' ]
    
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search