Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Joe Gores
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Joe Gores

Webpack 4 Automatically Makes process.env.NODE_ENV Available In Your JavaScript

By
Published in Comments (9)

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:

Webpack 4's DefinePlugin substitutes process.env.NODE_ENV with

And, if we run the Webpack build in "production" mode, we get the following output when we load the JavaScript application:

Webpack 4's DefinePlugin substitutes process.env.NODE_ENV with

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

448 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:

ng build

I think Angular builds the 'webpack' file for me, because I cannot see a 'webpack' file, anywhere in my project's folder.

So, is:

process.env.NODE_ENV

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?

15,848 Comments

@Charles,

Re:

And how are you able to reference this variable, in your 'main.ts'?

... 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:

if ( "production" === "production" ) {
	enableProdMode();
}

At runtime, there is no reference to process.env.NODE_ENV - it has been completely replaced during the compilation step.

15,848 Comments

@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:

Because your build script is itself most likely to be JavaScript code running under Node, it's going to have process.env.NODE_ENV available to it as it runs. Because so many tools and libraries already share the convention of using that field's value to determine their dev-vs-production status, the common convention is to use the current value of that field inside the build script as it's running to also determine the value of that field as applied to the client code being transformed.

7 Comments

I ran into the process.env.NODE_ENV problem when migrating an Angular Application to use Universal Rendering

There is an ng add @nguniversal/express-engine schematic - which worked very well. During the production build process, it uses webpack to bundle the Express server.js - when running the server.js file, and debugging it to a breakpoint, NODE_ENV would console.log out as none, but I could see it was production 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

15,848 Comments

@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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel