Skip to content

Instantly share code, notes, and snippets.

@robatwilliams
Created December 3, 2017 23:25
Show Gist options
  • Save robatwilliams/36a95119ae5adcd734a73f642f749cc3 to your computer and use it in GitHub Desktop.
Save robatwilliams/36a95119ae5adcd734a73f642f749cc3 to your computer and use it in GitHub Desktop.
Plugin to create HTML page which loads ES5 or ES6 build according to browser capability
const { concat, mapKeys, merge, uniq } = require('lodash');
/**
* Tweaked from original by Mike Engel
* https://github.com/jantimon/html-webpack-plugin/issues/782#issuecomment-331229728
*
* Use this with multiple Webpack configurations that generate different builds
* for modern and legacy browsers. But use the same instance of the plugin in both configurations.
*
* It keeps track of assets seen in each build configuration, and appends script tags for
* all the assets to subsequent builds - using type=module or nomodule to cause the appropriate
* version of each one to be loaded.
*
* The HTML file will be written for each configuration, but only the last-emitted one (which will
* overwrite any previous ones) will have all the asset tags.
*
* Many browsers (IE10, IE11, Firefox 57 at least) will download (but not run) both versions of
* every asset - see https://github.com/philipwalton/webpack-esnext-boilerplate/issues/1
*/
function HtmlModuleScriptWebpackPlugin(options) {
this.options = options;
this.sharedAssets = { js: [], chunks: [] };
}
HtmlModuleScriptWebpackPlugin.prototype = {
apply: function (compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin('html-webpack-plugin-before-html-processing', this.beforeHtmlProcessing.bind(this));
compilation.plugin('html-webpack-plugin-alter-asset-tags', this.alterAssetTags.bind(this));
});
},
beforeHtmlProcessing: function (htmlPluginData, callback) {
// Avoid chunk name collisions, since they can be named the same between builds
// { 'main': {} } to { 'main_modern': {} } if the filename matches
const renamedChunks = mapKeys(htmlPluginData.assets.chunks, (chunk, name) => {
if (chunk.entry.includes(this.options.entrySuffix)) {
return `${name}${this.options.entrySuffix}`;
}
return name;
});
this.sharedAssets = {
js: uniq(concat(this.sharedAssets.js, htmlPluginData.assets.js)),
chunks: merge(this.sharedAssets.chunks, renamedChunks)
};
callback(null, merge(htmlPluginData.assets, this.sharedAssets));
},
alterAssetTags: function (htmlPluginData, callback) {
const body = htmlPluginData.body.map(assetTag => {
const isModern = assetTag.attributes.src.includes(this.options.entrySuffix);
const attributes = isModern ? { type: 'module' }: { nomodule: true };
return merge(assetTag, { attributes });
});
callback(null, merge(htmlPluginData, { body }));
}
};
module.exports = HtmlModuleScriptWebpackPlugin;
const HtmlModuleScriptWebpackPlugin = require('./HtmlModuleScriptWebpackPlugin');
// Must be shared across configurations
const htmlModuleScriptPlugin = new HtmlModuleScriptWebpackPlugin({
entrySuffix: '_modern'
});
// in your plugins section:
const plugins = [
// Make the HTML page load a modern build if the browser supports it, otherwise a legacy one
htmlModuleScriptPlugin,
// Creates index.html
new HtmlWebpackPlugin({
favicon: './src/favicon.ico',
title: 'React Starter'
})
];
module.exports = [
modernConfig, // with entry named "main_modern"
legacyConfig
];
/*
* You'll also need the "_modern" suffix on any other entry points, such as CommonsChunkPlugin ones
* used for separating a vendor chunk and/or runtime(manifest) chunk.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment