Alejandro Napoles

Alejandro Napoles


Thoughts and ideas about programming, technology and my life as a lifelong learner and developer.

Share


Tags


Twitter


Migration to Webpack 2

With the release of Webpack 2 a few months back, it’s already a good time to start migrating to this new version. The first thing I noticed is the new documenta…

Alejandro NapolesAlejandro Napoles

With the release of Webpack 2 a few months back, it’s already a good time to start migrating to this new version. The first thing I noticed is the new documentation, it’s now way clearer and better overall than the old docs, it felt like anybody could understand and configure Webpack with minimal effort, kudos to them!

The new version, introduces, among others, two features:

  • ES2015 modules support and, internally, the code has been also refactor to ES2015. If you’re also transpiling Node code (Node.js doesn’t support ES2015 modules yet) or using other ES2015 features you’ll still need Babel.

  • Tree-shaking. Webpack will detect unused modules and remove them when minifying, to be able to use this, ES2015 modules need to process by Webpack, not Babel.

On top of these features, I like that a built-in validator is also included, which will check your config file and throw an error if anything is wrong in the schema.

Let’s go ahead and migrate a typical config, based on the previous article config, to the new version.

Rules

Rules are the new way of specifying loaders, the loaders property has been replaced with rules. They allow us to have an overview of each loader, and although it seems like a small change, in my opinion it shows them in a more readable manner, unifying the concept of loaders, getting rid of passing options through strings and establishing new simpler syntax.

Chaining loaders

Do you remember how we them before? There’s no need to use strings to do it anymore, now we define a use property inside a rule with all the loaders to be applied. The order of execution is from right to left, or bottom to top:

//Before

loaders: [
	{
		test:    /\.css$/,
		exclude: /node_modules/,
		loader:  "style-loader!css-loader"
	}
]

//After

rules: [
	{
		test:    /\.css$/,
		exclude: /node_modules/,
		use: [
			"style-loader",
			"css-loader"
		]
	}
]

No more pre or post loaders

All pre or post loaders are just regular ones, and therefore they all go inside rules. Now, if we want to define the category of the loader, we can explicitly do it with Rule.enforce:

//Before

preLoaders: [
	{
		test:    /\.js?$/,
		exclude: /node_modules/,
		loader:  "eslint-loader"
	}
],

loaders: [
//***
]
//After

rules: [
	{
		enforce: "pre",
		test:    /\.js?$/,
		exclude: /node_modules/,
		loader:  "eslint-loader"
	},
	//***
]

We could also chain them in order, so our loader gets executed before or after, in this example eslint-loader executes before babel-loader:

rules: [
	{
		test:    /\.js?$/,
		exclude: /node_modules/,
		use: [
			"babel-loader",
			"eslint-loader"
		]
	}
]

Passing options

Strings allowed us to chain loaders as well as passing options to them, e.g. "url-loader?limit=10000". I always thought it was a weird way to do things, especially from a readability standpoint. Fortunately, options can now be passed to them through, wait for it, an options property:

rules: [
	{
		test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
		loader:  "url-loader",
		options: {
			limit:    10000,
			mimetype: "application/font-woff"
		}
	}
]

This same syntax is also used to pass options in a chain:


//css-loader options to add CSS Modules support

rules: [
	{
		test:    /\.css$/,
		exclude: /node_modules/,
		use:     [
			"style-loader",
			{
				loader:  "css-loader",
				options: {
					importLoaders:  1,
					modules:        true,
					localIdentName: "[name]__[local]___[hash:base64:5]"
				}
			},
			"postcss-loader"
		]
	}
]

PostCSS and Autoprefixer

A while ago we used autoprefixer by passing the loader to the chain that was going to process css files: "style-loader!css-loader!autoprefixer-loader". Nowadays this loader is deprecated in favor of postcss-loader, since it is a plugin of PostCSS. There are two ways of adding Autoprefixer:

  • Pass it to the postcss-loader as an option.
{
	loader: "postcss-loader",
	options: {
	  plugins: function () {
		return [require("autoprefixer")];
      }
    }
}
  • Create a postcss.config.js file where we’ll define all the PostCSS related configuration:
module.exports = {
  plugins: [
    require("autoprefixer")
  ]
}

I went with the second option, since I think it’s cleaner this way, and having a specific PostCSS config file allows me to extend it in the future with new plugins without much of a hassle.

Once we have autoprefixer added to postcss-loader, we can set our supported browsers with Browserslists in package.json or its own browserslist config file. For example, the next snippet will support the last two versions for each major browser:

//package.json
{
  "browserslist": [
    "last 2 versions"
  ]
}

You can read more about how to configure postcss-loader in the postcss-loader-config Github page

Passing environment variables

In a previous article we talked about how to pass environment variables into Webpack with EnvironmentPlugin, to make sure that, when creating a production build, all the modules would run in production mode, not in development. No change is required here if you’re passing the environment variables in every script, but if process.env is undefined, you’d maybe like to have a default value for it. We can achieve this by passing an object to the plugin:

plugins: [
	new webpack.EnvironmentPlugin({
		NODE_ENV: "development",
		DEBUG: false
	})
]

Babel

One of the features of this release, as we’ve seen in the beginning of the article, is the support for ES2015 modules. We can now tell Babel to stop processing this modules, so Webpack can do it and tree-shaking can take place:

//.babelrc
{
  "presets": [
    ["es2015", {
		"modules": false
    }]
  ]
}

Although it’s not necessary, I’m using babel-preset-env, a preset that automatically determines the plugins based on the environment we have. This means there's no need to install or configure any extra plugins, babel-preset-env will take care of it for us.

With Browserslists or manually in the .babelrc config file we can configure the supported browsers. In my case, babel-preset-env will use my previous browserlists to determine the presets needed in the front-end.

Targeting Node.js and browsers

In a scenario in which we’re developing the back-end as well as the front-end, and we want Babel to transpile ES2015 modules in Node.js and at the same time to leave them to Webpack in the front-end, we could use two .babelrc files, one for targeting just Node.js and other to target browsers and add presets like React.

To make this work, we need to know the .babelrc lookup behavior:

Babel will look for a .babelrc in the current directory of the file being transpiled. If one does not exist, it will travel up the directory tree until it finds either a .babelrc, or a package.json with a "babel": {} hash within.

This behavior tells us that the front-end .babelrc will need to be close to the front-end files. If you take a look at the final Webpack config at the end of this article, you’ll see that the context is my client directory, so there’s where I’m going to place it:


{
  "presets": [
    "react",
    [
      "env",
      {
        "modules": false
      }
    ]
  ],
  "plugins": [
	//plugins...
  ]
}

Then, the other config will need to be placed up the directory tree to make it unreachable to babel-loader. I’ll create it in the root of the project, in order to make every non front-end file use this config if transpiling is needed:

{
  "presets": [
    [
      "env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ],
	"plugins": [
		//plugins...
  ]
}

Now we can have Babel transpiling the server files as well as letting Webpack process the ES2015 modules itself. To test this, setting the debug: true option in babel-preset-env, in both .babelrc files, will show us what targets and plugins are being used when running the app:

Using targets:
{
  "node": 7.8
}

Modules transform: commonjs

Using plugins:
  syntax-trailing-function-commas {"node":7.8}
babel-preset-env: `DEBUG` option

Using targets:
{}

Modules transform: false

Using plugins:
  transform-es2015-arrow-functions {}
	//More plugins...

//...
Version: webpack 2.3.2

Hot Reloading with Webpack-hot-middleware

Webpack 2 makes some changes to certain plugins related to hot reloading:

Therefore, in our previous development configuration file, we should only make changes to these plugins:

const webpack    = require("webpack"),
      baseConfig = require("./webpack.base.config.js");

baseConfig.entry = ["webpack-hot-middleware/client", "./main"];

baseConfig.output.publicPath = "/";

baseConfig.plugins = [
	new webpack.HotModuleReplacementPlugin(),
	new webpack.NoEmitOnErrorsPlugin()
];

module.exports = baseConfig;

Final Webpack 2 configuration

Once we finish the migration, we should have something similar to the config below. This is just a simple but working example to illustrate the entire process. You maybe have observed there’s no resolve, that’s because Webpack 2 provides reasonable defaults.

For more information on this topic go to the official docs and specially the migration guide, they’re both great!

const webpack = require("webpack"),
      path    = require("path");

const DIST_DIR   = path.resolve(__dirname, "dist"),
      CLIENT_DIR = path.resolve(__dirname, "src");

module.exports = {
	context: CLIENT_DIR,

	entry: "./main"

	output: {
		path:       DIST_DIR,
		filename:   "bundle.js",
		publicPath: "/"
	},

	module: {
		rules: [
			{
				test:    /\.js$/,
				exclude: /node_modules/,
				use: [
					"babel-loader",
					"eslint-loader"
				]
			},

			{
				test:    /\.css$/,
				exclude: /node_modules/,
				use:     [
					"style-loader",
					{
						loader:  "css-loader",
						options: {
							importLoaders:  1,
							modules:        true,
							localIdentName: "[name]__[local]___[hash:base64:5]"
						}
					},
					"postcss-loader"
				]
			},

			{
				test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
				loader:  "url-loader",
				options: {
					limit:    10000,
					mimetype: "application/font-woff"
				}
			},

			{
				test:   /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
				loader: "file-loader"
			}
		]
	},

	plugins: [
		new webpack.EnvironmentPlugin({
			NODE_ENV: "development",
			DEBUG: false
		})
	]
};
Comments