Webpack 4 Automatically Makes process.env.NODE_ENV Available In Your JavaScript
To be honest, I don't really understand how to use Webpack. In my Angular and Vue.js apps, I just borrow what I can from other examples; and then hack at it until something seems to "work." As such, it shouldn't be surprising whenever I stumble across an exciting Webpack feature. Which is exactly what happened this morning. I realized that Webpack 4's DefinePlugin automatically makes the "process.env.NODE_ENV" value available in my JavaScript and TypeScript applications. And, that it varies between "production" and "development" based on the mode in which Webpack 4 is being invoked.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
I only just discovered Webpack 4's DefinePlugin last week when building my file-sharing application with Netlify Lambda Functions. The DefinePlugin provides a mechanism by which substrings within your code can be found-and-replaced at build time. Essentially, it's a way to make dynamic code substitutions based on the build context.
Now, in last week's Netlify demo, I was explicitly adding the DefinePlugin to my webpack.config.js file. But, what I discovered this morning is that even if I omit the DefinePlugin from my configuration, Webpack automatically activates it; and, automatically provides a substitution for the string, "process.env.NODE_ENV".
The value that it substitutes for "process.env.NODE_ENV" is based on the mode in which Webpack was invoke. If you run the webpack-cli in "development" mode:
webpack --mode development --watch
... then the DefinePlugin will substitute the value, "development". And, if you run the webpack-cli in "production" mode:
webpack --mode production
... then the DefinePlugin will substitute the value, "production".
To see this in action, let's create a JavaScript application that only has a single file that outputs the "process.env.NODE_ENV" value:
// In Webpack 4, the DefinePlugin is automatically replacing "process.env.NODE_ENV" with
// either "production" or "development" based on the current build-mode. This gives your
// JavaScript code access to the contextual build-mode for conditional logic (such as
// configuring your Angular application to run in "prod" mode with enableProdMode()).
// --
// NOTE: To be clear, this is not making the "env" object available in JavaScript; this
// is doing a complete code-wide substitution of the substring "process.env.NODE_ENV".
console.group( "process.env.NODE_ENV" );
console.log( process.env.NODE_ENV );
console.groupEnd();
If we run the Webpack build in "development" mode, we get the following output when we load the JavaScript application:
And, if we run the Webpack build in "production" mode, we get the following output when we load the JavaScript application:
As you can see, Webpack's DefinePlugin altered our code to substitute the substring "process.env.NODE_ENV" with the Webpack build mode.
And, just to verify that I don't have the DefinePlugin included anywhere in my Webpack config, here's my webpack.config.js file:
// Load the core node modules.
var CleanWebpackPlugin = require( "clean-webpack-plugin" );
var HtmlWebpackPlugin = require( "html-webpack-plugin" );
var path = require( "path" );
var webpack = require( "webpack" );
// We are exporting a Function instead of a configuration object so that we can
// dynamically define the configuration object based on the execution mode.
module.exports = ( env, argv ) => {
var isDevelopmentMode = ( argv.mode === "development" );
// Locally, we want robust source-maps. However, in production, we want something
// that can help with debugging without giving away all of the source-code. This
// production setting will give us proper file-names and line-numbers for debugging;
// but, without actually providing any code content.
var devtool = isDevelopmentMode
? "eval-source-map"
: "nosources-source-map"
;
// By default, each module is identified based on Webpack's internal ordering. This
// can cause issues for cache-busting and long-term browser caching as a localized
// change can create a rippling effect on module identifiers. As such, we want to
// identify modules based on a name that is order-independent. Both of the following
// plugins do roughly the same thing; only, the one in development provides a longer
// and more clear ID.
var moduleIdentifierPlugin = isDevelopmentMode
? new webpack.NamedModulesPlugin()
: new webpack.HashedModuleIdsPlugin()
;
return({
// I define the base-bundles that will be generated.
// --
// NOTE: There is no explicit "vendor" bundle. With Webpack 4, that level of
// separation is handled by default. You just include your entry bundle and
// Webpack's splitChunks optimization DEFAULTS will automatically separate out
// modules that are in the "node_modules" folder.
// --
// CAUTION: The ORDER OF THESE KEYS is meaningful "by coincidence." Technically,
// the order of keys in a JavaScript object shouldn't make a difference because,
// TECHNICALLY, the JavaScript language makes to guarantees around key ordering.
// However, from a practical standpoint, JavaScript keys are iterated over in the
// same order in which they were defined (especially in V8). By putting the
// POLYFILL bundle first in the object definition, it will cause the polyfill
// bundle to be injected into the generated HTML file first. If you don't want to
// rely on this ordering - or, if it breaks for you anyway - you can use the
// HtmlWebpackPlugin (see: chunksSortMode) to explicitly order chunks.
entry: {
main: "./app/main.js"
},
// I define the bundle file-name scheme.
output: {
filename: "[name].[contenthash].js",
path: path.join( __dirname, "build" ),
publicPath: "./build/"
},
devtool: devtool,
resolve: {
extensions: [ ".js" ],
alias: {
"~/app": path.resolve( __dirname, "app" )
}
},
module: {
rules: []
},
plugins: [
// I clean the build directory before each build.
new CleanWebpackPlugin(),
// I generate the main "index" file and inject Script tags for the files
// emitted by the compilation process.
new HtmlWebpackPlugin({
// Notice that we are saving the index UP ONE DIRECTORY, so that it is
// output in the root of the demo.
filename: "../index.htm",
template: "./app/main.htm"
}),
// I facilitate better caching for generated bundles.
moduleIdentifierPlugin
],
optimization: {
splitChunks: {
// Apply optimizations to all chunks, even initial ones (not just the
// ones that are lazy-loaded).
chunks: "all"
},
// I pull the Webpack runtime out into its own bundle file so that the
// contentHash of each subsequent bundle will remain the same as long as the
// source code of said bundles remain the same.
runtimeChunk: "single"
}
});
};
In my world, being able to differentiate between the "development" and "production" builds within my code is actually useful because one can improve Angular's performance in production by telling it to run less validation on the change-detection. As such, I will happily consume the "process.env.NODE_ENV" value during the bootstrapping of my Angular applications:
// Import the core angular services.
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
// Import the root module for bootstrapping.
import { AppModule } from "./app.module";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// NOTE: This ENV value is being provided by Webpack's DefinePlugin. It is populated
// based on the mode in which the webpack-cli was invoked.
if ( process.env.NODE_ENV === "production" ) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule( AppModule );
As you can see here, when the code detects that it was built for "production", it enables the "prod mode" in the Angular platform.
This may have been completely obvious to people who are familiar with Webpack. But, unfortunately, I'm not one of those people. So for me, Webpack is still a magical black box. That said, I'm very excited to see that Webpack 4 will automatically substitute the string, "process.env.NODE_ENV", with the current build mode as it compiles my JavaScript application. This will make it easy for me to improve the performance of my Angular applications.
Want to use code from this post? Check out the license.
Reader Comments
I have to say, when it comes to webpack, you are several steps ahead of me.
I don't really have a clue what it does:(
When, I run:
I think Angular builds the 'webpack' file for me, because I cannot see a 'webpack' file, anywhere in my project's folder.
So, is:
A node 'global' variable?
And how are you able to reference this variable, in your 'main.ts'?
Do you have to provide a path reference to a node file, in your Angular file, or something?
I wrote a post a couple years ago that answers those questions:
Building Better Bundles: Why process.env.NODE_ENV Matters for Optimized Builds
@Charles,
Re:
... that's exactly what I'm trying to get at in this post. Webpack is making this available automatically using its own
DefinePlugin
(which we don't even reference in this particular demo). Essentially, as Webpack is compiling your code, it is looking for the string,process.env.NODE_ENV
. And, if it finds it, it replaces is in your source code with the value"production"
or"development"
. So, when your Angular code runs at runtime, it's actually executing something that looks more like this:At runtime, there is no reference to
process.env.NODE_ENV
- it has been completely replaced during the compilation step.@Mark,
Good write-up. And
@Charles,
Mark has a good description in his linked-post as to where the
NODE_ENV
value is coming from:Cheers. I will have a look at this link...
I ran into the
process.env.NODE_ENV
problem when migrating an Angular Application to use Universal RenderingThere is an
ng add @nguniversal/express-engine
schematic - which worked very well. During the production build process, it useswebpack
to bundle the Expressserver.js
- when running the server.js file, and debugging it to a breakpoint,NODE_ENV
wouldconsole.log
out asnone
, but I could see it wasproduction
in the debug panel.Took me hours to find that webpack had replaced every occurrence of the text
process.env.NODE_ENV
with"none"
- and that I had to use DefinePlugin@Brian,
That sounds like a sticky situation. I've only just begun to try using the Angular CLI -- the idea of doing any Universal Rendering feels like light-years in the future. I'm happy to have this tip in my back-pocket for when I finally get there.
Thanks a lot! you realy help.
@Dmitriy,
Awesome, happy to hear it :D