Polymer and Webpack

I’ve started using Webpack in my build process and I must say, I’m quite impressed. So over the past couple of years my build process has migrated a bit. I started by using Grunt, moved to Gulp and then created my own build process to address some custom items working with Red Pill’s infrastructures and Domino. But since finding Webpack, I’m finding that it does a much better job, with much less effort.

So, when I built my own build process, this consisted of many google polymer project dependencies and configuration and a few hundred lines of code. While this worked, and still does, the ease of use of Webpack is much better and less prone to error and providing consistent results. All this by just filling out a configuration file.

So, lets look at an example. We recently built a very simple application for a customer using Polymer 3 and the build system was Webpack. We have 3 Webpack config files, one is a common config file (webpack.common.js), next is a dev config file (webpack.dev.js) and finally the prod config file (webpack.prod.js). For brevity I’m only going to cover the common and prod configuration files as that’s where the meat is. Here is our webpack.common.js file:

const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
	entry: {
		zion: './src/elements/zion-app.ts',
		view1: './src/elements/zion-view1.ts',
		view2: './src/elements/zion-view2.ts',
		view3: './src/elements/zion-view3.ts'
	},
	resolve: {
		extensions: ['.ts', '.js', '.json']
	},
	module: {
		rules: [
			{
				test: /\.ts$/,
				use: [
					'ts-loader'
				],
				include: path.resolve(__dirname, 'src')
			},{
				test: /\.html$/,
				use: 'html-loader'
			}
		]
	},
	plugins: [
		new CopyWebpackPlugin([
			{
				from: path.resolve(__dirname, 'src', 'images') + '/',
				to: path.resolve(__dirname, 'dist', 'zion', 'images'),
				toType: 'dir'
			},{
				from: path.resolve(__dirname, 'src', 'fonts') + '/',
				to: path.resolve(__dirname, 'dist', 'zion', 'fonts'),
				toType: 'dir'
			},{
				from: './src/*.json',
				to: path.resolve(__dirname, 'dist', 'zion') + '/',
				flatten: true
			}
		])
	]
};

To break down what this does we’ll start with the entry property. This tells webpack what all of our entry points into the app will be. We list every page someone can land on. These will be split out into their own bundles.

Next is the module property. This property is where we define loaders. Loaders will parse our code and possibly transform it in some manner. For example, our first loader checks filename extensions and when it encounters a .ts file it will use the ts-loader to compile our TypeScript into JavaScript, but only if that .ts file is in the src directory. This is where the power of webpack really shines. There are countless loaders out there for all types of scenarios. You just define it here and it should just work for the most part.

Next up is the plugins property. This is where you can define webpack plugins to use. In our case we’re using the copy-webpack-plugin. This plugin will copy files from one place to another. In our case we want to copy images, fonts and static JSON files into our dist directory.

The webpack.common.js file is the base of our webpack configuration. All other webpack.config files will use this file as their starting points, but I’ll get to that in a moment. Here is our webpack.prod.js file:

const path = require('path');
const webPackCommon = require('./webpack.common');
const merge = require('webpack-merge');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');

const isAnalyze = typeof process.env.BUNDLE_ANALYZE !== 'undefined';

const config = merge(webPackCommon, {
	output: {
		filename: '[name]-[contentHash]-bundle.js',
		path: path.resolve(__dirname, 'dist','zion'),
		publicPath: '/zion/'
	},
	devtool: 'source-map',
	mode: 'production',
	module: {
		rules: [
			{
				test: /\.(png|svg|jp(e*)g|gif)$/,
				use: {
					loader: 'file-loader',
					options: {
						name: '[name]-[hash].[ext]',
						outputPath: 'images'
					}
				}
			}
		]
	},
	optimization: {
		splitChunks: {
			chunks: 'async',
			minChunks: 1,
			minSize: 30000,
			name: true,
			automaticNameDelimiter: '-',
			cacheGroups: {
				polymer: {test: /[\\/]node_modules[\\/](@polymer).*[\\/]/, name: 'polymer', chunks: 'all'},
				vaadin: {test: /[\\/]node_modules[\\/](@vaadin).*[\\/]/, name: 'vaadin', chunks: 'all'},
				webcomponents: {test: /[\\/]node_modules[\\/](@webcomponents).*[\\/]/, name: 'webcomponents', chunks: 'all'},
				commons: {test: /[\\/]node_modules[\\/](?!(@polymer|@vaadin|@webcomponents)).*[\\/]/, name: 'commons', chunks: 'all'}
			}
		},
		minimizer: [
			new TerserPlugin({
				terserOptions: {
					output: {
						comments: false
					}
				},
				sourceMap: true,
				parallel: true
			}),
			new HtmlWebpackPlugin({
				template: './src/index.html',
				minify: {
					collapseWhitespace: true,
					removeComments: true,
					minifyCSS: true,
					minifyJS: true
				}
			})
		]
	},
	plugins: [
		new CleanWebpackPlugin(['dist']),
		new SWPrecacheWebpackPlugin({
			cacheID: 'zion-precache',
			dontCacheBustUrlsMatching: /-bundle\.js$/,
			filename: 'service-worker.js',
			minify: true,
			navigateFallback: '/zion/index.html',
			staticFileGlobsIgnorePatterns: [/\.map$/],
			ignoreUrlParametersMatching: [],
			runtimeCaching: [
				{urlPattern: /\/api\/oda\/(frame[s]?|command|info|esearch)\/now/, handler: 'networkOnly', method: 'put'},
				{urlPattern: /\/api\/oda\/(frame[s]?|command|info|esearch)\/now/, handler: 'networkOnly', method: 'post'},
				{urlPattern: /\/names.nsf/, handler: 'networkOnly', method: 'post'},
				{urlPattern: /\/api\/oda\/(frame[s]?|command|info|esearch)\/now/, handler: 'networkOnly', method: 'delete'},
				{urlPattern: /\/api\/oda\/(frame[s]?|command|info|esearch)\/now/, handler: 'networkOnly', method: 'head'},
				{urlPattern: /type=com\.redpill\.model\./, handler: 'fastest', method: 'get'},
				{urlPattern: /&vertices=/, handler: 'fastest', method: 'get'},
				{urlPattern: /\/settings.json$/, handler: 'fastest', method: 'get'},
				{urlPattern: /\/now\?id=[a-fA-F0-9]{16}[0]{32}$/, handler: 'fastest', method: 'get'}
			]
		})
	]
});

if (isAnalyze) {
	config.plugins.push(new BundleAnalyzerPlugin());
}

module.exports = config;

Now, lets break this one down a little bit. Notice there isn’t an entry property? Well, this property is being provided by the webpack.common.js file. This functionality is provided by the webpack-merge module and the webpack.common.js file is being imported as a module on the second line of this file. Now look at the line const config = merge(webPackCommon, {...}) bit. That is how we’re merging our webpack.common.js file into this file. Which is, well…. cool!

Onto breaking down the config a bit more. First up is the output property. This defines how our entry files will be output to the dist directory. There are a few options in here. filename is what we’ll change the default name of our entry point to. We’ll just take the name of the entry [name], add a -[contentHash]- to it and finally tack on bundle.js. So let’s look at the very first item in our entry property in webpack.common.js. It’s got a key name of zion and a value of ./src/elements/zion-app.ts. This will now get changed to ./dist/zion/zion-ff0449569908df850fd3-bundle.js. That content hash will only change if the content of that bundle changes. So we get cache busting out of the box that’s really simple to implement. Next output property is the path which defines the path we want to dump all of our bundles to. Finally there is the publicPath. This defines the public path of our application, in this case it’s zion. So, the path to our app will be https://some.host.name/zion.

Next property is mode and it’s value is production. This will automatically ensure our code is minified. This works in conjunction with the optimization.minimizer property which I’ll get to in a second.

Next up is the module property. Notice this looks pretty much like the module property in webpack.common.js. The only difference is we’re including a hash in the file name. Again, that hash will only change if the image changes.

Now the optimization property. We’ve got 2 properties here. splitChunks which will take our “vendor” code and split them into a few different chunks. These bundles will follow the same naming pattern as defined in the output property. The last bit of this property is the minimizer property which minifies our JavaScript and HTML, including inline styles and JavaScript.

Our last property here is the plugins property. I’ve defined two plugins here. The first one is the CleanWebpackPlugin which will delete the dist directory when the build is first executed. The second configures the service-worker.js file

The last bit to be concerned with in the webpack.prod.js file just checks if an environment variable (BUNDLE_ANALYZE) is set and if it is, pushes the BundleAnalyzerPlugin to the plugins property. And finally, we export the configuration.

The last thing of concern here is our package.json file. Specifically the scripts property of that file:

"scripts": {
		"start": "babel-node ./node_modules/webpack-dev-server/bin/webpack-dev-server --config webpack.dev.js --open",
		"compile": "npm run build",
		"build": "babel-node ./node_modules/webpack/bin/webpack --config webpack.prod.js",
		"build:analyze": "BUNDLE_ANALYZE=true webpack --config webpack.prod.js"
	}

The scripts of concern here are start, build and build:analyze. All of these scripts define the webpack config file to use. Notice you don’t see webpack.common.js anywhere? This is because everything “merges” this file into it. But let’s start with the start script. This fires up the development server and uses the webpack.dev.js file. Next up is build, which uses our webpack.prod.js configuration file. Lastly is build:analyze which sets the BUNDLE_ANALYZE environment variable to true. Which if you recall, adds the BundleAnalyzerPlugin to the production plugins.

I know, this all looks and sounds rather intimidating, but believe me it’s much simpler than trying to write all this functionality yourself or piece it together in a gulpfile.js or something. Not to mention the consistency of the build process just got a lot better than some home grown or chained together tasks.

Until next time…. Happy Coding!

Share This:

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.