Conditional build based on environment using Webpack
Asked Answered
G

10

109

I have some things for development - e.g mocks which I would like to not bloat my distributed build file with.

In RequireJS you can pass a config in a plugin file and conditonally require things in based on that.

For webpack there doesn't seem to be a way of doing this. Firstly to create a runtime config for an environment I have used resolve.alias to repoint a require depending on the environment, e.g:

// All settings.
var all = {
    fish: 'salmon'
};

// `envsettings` is an alias resolved at build time.
module.exports = Object.assign(all, require('envsettings'));

Then when creating the webpack config I can dynamically assign which file envsettings points to (i.e. webpackConfig.resolve.alias.envsettings = './' + env).

However I would like to do something like:

if (settings.mock) {
    // Short-circuit ajax calls.
    // Require in all the mock modules.
}

But obviously I don't want to build in those mock files if the environment isn't mock.

I could possibly manually repoint all those requires to a stub file using resolve.alias again - but is there a way that feels less hacky?

Any ideas how I can do that? Thanks.

Godson answered 17/2, 2015 at 22:29 Comment(1)
Note that for now I have used alias's to point to an empty (stub) file on environments I don't want (e.g. require('mocks') will point to an empty file on non-mock envs. Seems a little hacky but it works.Godson
H
65

You can use the define plugin.

I use it by doing something as simple as this in your webpack build file where env is the path to a file that exports an object of settings:

// Webpack build config
plugins: [
    new webpack.DefinePlugin({
        ENV: require(path.join(__dirname, './path-to-env-files/', env))
    })
]

// Settings file located at `path-to-env-files/dev.js`
module.exports = { debug: true };

and then this in your code

if (ENV.debug) {
    console.log('Yo!');
}

It will strip this code out of your build file if the condition is false. You can see a working Webpack build example here.

Honeysweet answered 24/4, 2015 at 15:16 Comment(9)
I'm a little confused by this solution. It doesn't mention how I'm supposed to set env. Looking through that example it seems as though they're handling that flag via gulp and yargs which not everyone is using.Bilyeu
How does this work with linters? Do you have to manually define new global variables that are added in the Define plugin?Coimbatore
@Coimbatore yes. Add something like "globals": { "ENV": true } to your .eslintrcHoneysweet
how would i access the ENV variable in a component? I tried the solution above but I still get the error that ENV is not definedYolondayon
You should be able to access the ENV variable just fine. It does require you to re-run Webpack if you just added the webpack.DefinePlugin though...Honeysweet
If you want to pass your own variable, you can do this via var foo = "bar"; ... webpack.DefinePlugin({ENV: JSON.stringify(foo)}) ....Loudhailer
You should use JSON.stringify on all of the defined values. Webpack inserts them 'as-is'. See here: github.com/webpack/docs/wiki/list-of-plugins#defineplugin. It should be rather: ENV: JSON.stringify(require(path.join(__dirname, './path-to-env-files/', env)))Slipway
It does NOT strip the code out of the build files ! I tested it and the code is here.Shark
@Shark use babeljs.io/docs/plugins/minify-dead-code-elimination if you want to strip out the code.Preview
L
56

Not sure why the "webpack.DefinePlugin" answer is the top one everywhere for defining Environment based imports/requires.

The problem with that approach is that you are still delivering all those modules to the client -> check with webpack-bundle-analyezer for instance. And not reducing your bundle.js's size at all :)

So what really works well and much more logical is: NormalModuleReplacementPlugin

So rather than do a on_client conditional require -> just not include not needed files to the bundle in the first place

Hope that helps

Lepidolite answered 9/7, 2017 at 8:30 Comment(4)
Nice didn't know about that plugin!Godson
With this scenario wouldn't you have multiple builds per environment? For example if I have web service address for dev/QA/UAT/production environments I would then need 4 separate containers, 1 for each environment. Ideally you would have one container and launch it with an environment variable to specify which config to load.Isolation
No, not really. That's exactly what you do with the plugin -> you specify your environment through env vars and it builds only one container, but for particular environment without redundant inclusions. Off course that also depends on how you setup your webpack config and obviously you can build all the builds, but it's not what this plugin is about and does.Lepidolite
@RomanZhyliov What if I need to import a npm package based on client side errors. I think this plugin won't work, right?Sibylsibylla
V
53

Use ifdef-loader. In your source files you can do stuff like

/// #if ENV === 'production'
console.log('production!');
/// #endif

The relevant webpack configuration is

const preprocessor = {
  ENV: process.env.NODE_ENV || 'development',
};

const ifdef_query = require('querystring').encode({ json: JSON.stringify(preprocessor) });

const config = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: `ifdef-loader?${ifdef_query}`,
        },
      },
    ],
  },
  // ...
};
Valora answered 5/5, 2017 at 22:56 Comment(2)
I upvoted this answer since the accepted answer does not strip out code as expected and the preprocessor-like syntax is more likely to be identified as a conditional element.Melgar
Thanks so much! It works like a charm. Several hours of experiments with ContextReplacementPlugin, NormalModuleReplacementPlugin, and other stuff – all failed. And here is ifdef-loader, saving my day.Valeryvalerye
S
31

I ended up using something similar to Matt Derrick' Answer, but was worried about two points:

  1. The complete config is injected every time I use ENV (Which is bad for large configs).
  2. I have to define multiple entry points because require(env) points to different files.

What I came up with is a simple composer which builds a config object and injects it to a config module.
Here is the file structure, Iam using for this:

config/
 └── main.js
 └── dev.js
 └── production.js
src/
 └── app.js
 └── config.js
 └── ...
webpack.config.js

The main.js holds all default config stuff:

// main.js
const mainConfig = {
  apiEndPoint: 'https://api.example.com',
  ...
}

module.exports = mainConfig;

The dev.js and production.js only hold config stuff which overrides the main config:

// dev.js
const devConfig = {
  apiEndPoint: 'http://localhost:4000'
}

module.exports = devConfig;

The important part is the webpack.config.js which composes the config and uses the DefinePlugin to generate a environment variable __APP_CONFIG__ which holds the composed config object:

const argv = require('yargs').argv;
const _ = require('lodash');
const webpack = require('webpack');

// Import all app configs
const appConfig = require('./config/main');
const appConfigDev = require('./config/dev');
const appConfigProduction = require('./config/production');

const ENV = argv.env || 'dev';

function composeConfig(env) {
  if (env === 'dev') {
    return _.merge({}, appConfig, appConfigDev);
  }

  if (env === 'production') {
    return _.merge({}, appConfig, appConfigProduction);
  }
}

// Webpack config object
module.exports = {
  entry: './src/app.js',
  ...
  plugins: [
    new webpack.DefinePlugin({
      __APP_CONFIG__: JSON.stringify(composeConfig(ENV))
    })
  ]
};

The last step is now the config.js, it looks like this (Using es6 import export syntax here because its under webpack):

const config = __APP_CONFIG__;

export default config;

In your app.js you could now use import config from './config'; to get the config object.

Skaw answered 16/4, 2017 at 10:29 Comment(1)
Really the best answer hereRubin
H
18

another way is using a JS file as a proxy, and let that file load the module of interest in commonjs, and export it as es2015 module, like this:

// file: myModule.dev.js
module.exports = "this is in dev"

// file: myModule.prod.js
module.exports = "this is in prod"

// file: myModule.js
let loadedModule
if(WEBPACK_IS_DEVELOPMENT){
    loadedModule = require('./myModule.dev.js')
}else{
    loadedModule = require('./myModule.prod.js')
}

export const myString = loadedModule

Then you can use ES2015 module in your app normally:

// myApp.js
import { myString } from './store/myModule.js'
myString // <- "this is in dev"
Hudgins answered 9/1, 2016 at 4:23 Comment(4)
The only problem with if/else and require is that both required files will be bundled into the generated file. I haven't found a workaround. Essentially bundling happens first, then mangling.Swiercz
that's not necesary true, if you use in your webpack file the plugin webpack.optimize.UglifyJsPlugin(), the optimization of webpack won't load the module, as the line code inside the conditional is always false, so webpack remove it from the generated bundleHudgins
@AlejandroSilva do you have a repo example of this?Jubilate
@thevangelist yep: github.com/AlejandroSilva/mototracker/blob/master/… it's a node+react+redux pet proyect :PHudgins
U
5

Faced with the same problem as the OP and required, because of licensing, not to include certain code in certain builds, I adopted the webpack-conditional-loader as follows:

In my build command I set an environment variable appropriately for my build. For example 'demo' in package.json:

...
  "scripts": {
    ...
    "buildDemo": "./node_modules/.bin/webpack --config webpack.config/demo.js --env.demo --progress --colors",
...

The confusing bit that is missing from the documentation I read is that I have to make this visible throughout the build processing by ensuring my env variable gets injected into the process global thus in my webpack.config/demo.js:

/* The demo includes project/reports action to access placeholder graphs.
This is achieved by using the webpack-conditional-loader process.env.demo === true
 */

const config = require('./production.js');
config.optimization = {...(config.optimization || {}), minimize: false};

module.exports = env => {
  process.env = {...(process.env || {}), ...env};
  return config};

With this in place, I can conditionally exclude anything, ensuring that any related code is properly shaken out of the resulting JavaScript. For example in my routes.js the demo content is kept out of other builds thus:

...
// #if process.env.demo
import Reports from 'components/model/project/reports';
// #endif
...
const routeMap = [
  ...
  // #if process.env.demo
  {path: "/project/reports/:id", component: Reports},
  // #endif
...

This works with webpack 4.29.6.

Untuck answered 22/4, 2019 at 20:33 Comment(2)
There is also github.com/dearrrfish/preprocess-loader which has more featuresFecund
i wonder why these approaches aren't that popular. it's quite nice to compile differently for purposesAllayne
S
1

I've struggled with setting env in my webpack configs. What I usually want is to set env so that it can be reached inside webpack.config.js, postcss.config.js and inside the entry point application itself (index.js usually). I hope that my findings can help someone.

The solution that I've come up with is to pass in --env production or --env development, and then set mode inside webpack.config.js. However, that doesn't help me with making env accessible where I want it (see above), so I also need to set process.env.NODE_ENV explicitly, as recommended here. Most relevant part that I have in webpack.config.js follow below.

...
module.exports = mode => {
  process.env.NODE_ENV = mode;

  if (mode === "production") {
    return merge(commonConfig, productionConfig, { mode });
  }
  return merge(commonConfig, developmentConfig, { mode });
};
Sober answered 7/4, 2019 at 19:55 Comment(0)
F
0

Use envirnment variables to create dev and prod deployments:

https://webpack.js.org/guides/environment-variables/

Factoring answered 17/1, 2018 at 15:23 Comment(2)
This isn't what I was askingGodson
the problem is that webpack will ignore the condition when building the bundle and will include code loaded for development anyways... so it doesn't solve the issueThaw
G
0

I use string-replace-loader to get rid of an unnecessary import from the production build, and it works as expected: the bundle size becomes less, and a module for development purposes (redux-logger) is completely removed from it. Here is the simplified code:

  • In the file webpack.config.js:
rules: [
  // ... ,
  !env.dev && {
    test: /src\/store\/index\.js$/,
    loader: 'string-replace-loader',
    options: {
      search: /import.+createLogger.+from.+redux-logger.+;/,
      replace: 'const createLogger = null;',
    }
  }
].filter(Boolean)
  • In the file src/store/index.js:
// in prod this import declaration is substituted by `const createLogger = null`:
import { createLogger } from 'redux-logger';
// ...
export const store = configureStore({
  reducer: persistedReducer,
  middleware: createLogger ? [createLogger()] : [],
  devTools: !!createLogger
});
Gonzalo answered 4/5, 2022 at 8:50 Comment(0)
E
-2

While this is not the best solution, it may work for some of your needs. If you want to run different code in node and browser using this worked for me:

if (typeof window !== 'undefined') 
    return
}
//run node only code now
Extempore answered 6/11, 2017 at 10:40 Comment(1)
OP is asking about a compile time decision, your answer is about run time.Smidgen

© 2022 - 2024 — McMap. All rights reserved.