├── .npmignore ├── LICENSE.md ├── README.md ├── example ├── .babelrc ├── .gitignore ├── README.md ├── package.json ├── src │ ├── components │ │ └── App.tsx │ └── index.tsx ├── tsconfig.json ├── tsconfig.legacy.json ├── webpack.config.js ├── webpack │ ├── base.js │ ├── dev.js │ ├── legacyConfig.js │ ├── multi.js │ ├── prod.js │ └── prod.legacy.js └── yarn.lock ├── index.js ├── package.json └── template.ejs /.npmignore: -------------------------------------------------------------------------------- 1 | example/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tristan Teufel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # html-webpack-multi-build-plugin 2 | 3 | This plugin simplifies the creation of script tags for module and nomodule for a webpack multi build configuration. 4 | 5 | [![npm version](https://badge.fury.io/js/html-webpack-multi-build-plugin.svg)](http://badge.fury.io/js/html-webpack-multi-build-plugin) 6 | 7 | ### Proof of Concept 8 | 9 | Please see this project more as a proof of concept then a fully fledged solution. 10 | 11 | One day my supervisor gave me the following task: 12 | I should create a build job which creates two bundles. 13 | One bundle with polyfills for older browser, and one bundle with modern-js for newer browsers. 14 | So we researched for ways to achieve this. 15 | This respository contains a POC and all information we researched to this topic. 16 | We even successully used the plugin in a few projects. 17 | 18 | If you think something is wrong with this approach please do not hesitate to create a issue. 19 | If you have an idea how to make this better, we are very happy about any clarification or contribution. 20 | We know there are currently some down sides like the disabled preloading. 21 | 22 | ### Why do you want to do this? 23 | 24 | Most developers still transpile their code to ES5 and bundle it with polyfills to provide support for older browsers. 25 | But for newer browsers this transpiled code is unnecessary and probably slower then ES6+ code. 26 | The Idea is to create two bundles, one modern es6+ bundle and one legacy es5 bundle. 27 | 28 | ### What? How do you do that? 29 | The solution is to provide two script tags, one with type=module (es6+ code) and one with "nomodule" (es5 code). 30 | Modern Browser will now only load the script tag with type=module while legacy browser only load the script tag with "nomodule". 31 | 32 | ### Will some browser still download both bundles? 33 | 34 | Some browser like Safari, IE11, Edge are downloading both bundles, but only executing one. 35 | This plugin has integrated a clever fix for this. 36 | By creating a script tag with module / nomodule which dynamically injects the actual script tags for the javascript resources. 37 | 38 | ``` 39 | 45 | ``` 46 | 47 | ### Why do i need this addon? 48 | This plugin for html-webpack-plugin generates script tags for module and nomodule for a webpack multi build configuration. 49 | 50 | ### Async CSS Loading 51 | The included template add's tags for async (non blocking) css 52 | ``` 53 | 54 | ``` 55 | 56 | ### Read about webpack multi build configuration 57 | https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations 58 | 59 | ### How to use this addon? 60 | Check out my [Example Project](https://github.com/firsttris/html-webpack-multi-build-plugin/tree/master/example) 61 | 62 | Summarized 63 | 64 | #### Package.json 65 | ``` 66 | "scripts": { 67 | build:multi": "webpack --env.build=multi" 68 | } 69 | ``` 70 | 71 | #### webpack.config 72 | ``` 73 | // Legacy webpack config needs to include 'legacy' 74 | config.output.filename = '[name]_legacy.js'; 75 | 76 | // Modern webpack config must not include 'legacy' 77 | config.output.filename = '[name].js'; 78 | 79 | // Both webpack configs must include htmlWebpackPlugin and htmlWebpackMultiBuildPlugin 80 | const htmlWebpackMultiBuildPlugin = require('html-webpack-multi-build-plugin'); 81 | const multiBuildMode = process.env.build === 'multi' 82 | const template = multiBuildMode 83 | ? require.resolve('html-webpack-multi-build-plugin/template.ejs') 84 | : require.resolve('html-webpack-plugin/default_index.ejs'); 85 | 86 | config.plugins: [ 87 | new htmlWebpackPlugin( 88 | { 89 | inject: !multiBuildMode, 90 | template 91 | } 92 | ) 93 | new htmlWebpackMultiBuildPlugin() 94 | ] 95 | ``` 96 | 97 | 98 | ### Sources 99 | 100 | https://philipwalton.com/articles/deploying-es2015-code-in-production-today/ 101 | https://github.com/philipwalton/webpack-esnext-boilerplate 102 | https://jakearchibald.com/2017/es-modules-in-browsers/ 103 | https://github.com/jantimon/html-webpack-plugin/issues/782 104 | https://github.com/philipwalton/webpack-esnext-boilerplate/issues/1 105 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | .awcache/ 4 | 5 | # production 6 | build/ 7 | builds/ 8 | coverage/ 9 | .idea/ 10 | dist/ 11 | dist-web/ 12 | dist-electron/ 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | lerna-debug.log* 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | db 27 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # webpack-multi-build-example -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-build-starter", 3 | "version": "1.0.0", 4 | "description": "example of multi build", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --env.build=dev", 8 | "start:legacy": "webpack-dev-server --env.build=dev --env.legacy=true", 9 | "build": "webpack --env.build=prod", 10 | "build:legacy": "webpack --env.build=prod --env.legacy=true", 11 | "build:multi": "webpack --env.build=multi" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "" 16 | }, 17 | "keywords": [ 18 | "wp4", 19 | "multi-build", 20 | "starter-kit", 21 | "module", 22 | "nomodule" 23 | ], 24 | "devDependencies": { 25 | "@types/react": "^16.4.6", 26 | "@types/react-dom": "^16.0.6", 27 | "babel-core": "^6.26.3", 28 | "babel-loader": "^7.1.4", 29 | "babel-polyfill": "^6.26.0", 30 | "babel-preset-env": "^1.7.0", 31 | "clean-webpack-plugin": "^0.1.19", 32 | "css-loader": "^0.28.11", 33 | "file-loader": "^1.1.11", 34 | "fork-ts-checker-webpack-plugin": "^0.4.2", 35 | "html-webpack-plugin": "^3.2.0", 36 | "html-webpack-multi-build-plugin": "firsttris/html-webpack-multi-build-plugin#master", 37 | "react": "^16.4.1", 38 | "react-dom": "^16.4.1", 39 | "react-hot-loader": "^4.3.3", 40 | "ts-loader": "^4.4.2", 41 | "typescript": "^2.9.2", 42 | "webpack": "^4.15.0", 43 | "webpack-cli": "^3.0.8", 44 | "webpack-dev-server": "^3.1.4", 45 | "webpack-merge": "^4.1.3" 46 | }, 47 | "author": "tristan teufel", 48 | "license": "MIT" 49 | } 50 | -------------------------------------------------------------------------------- /example/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export class App extends React.Component<{}, {}> { 4 | render() { 5 | return

Webpack Multi Build Example

; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { hot } from 'react-hot-loader'; 4 | import { App } from './components/App'; 5 | 6 | const element = document.createElement('div'); 7 | element.id = 'root' 8 | document.body.appendChild(element); 9 | 10 | const app = render(, document.getElementById('root')); 11 | export default (Object.is(process.env.NODE_ENV, 'production') ? app : hot(module)(app)); 12 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "es2015", "es2016"], 5 | "pretty": true, 6 | "module": "commonjs", 7 | "allowJs": true, 8 | "jsx": "react", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "strict": true, 12 | "removeComments": true, 13 | "noImplicitAny": true, 14 | "noUnusedLocals": true, 15 | "moduleResolution": "node", 16 | "baseUrl": "." 17 | } 18 | } -------------------------------------------------------------------------------- /example/tsconfig.legacy.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(env) { 2 | process.env = env; 3 | return require(`./webpack/${env.build}.js`) 4 | }; -------------------------------------------------------------------------------- /example/webpack/base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'), 2 | merge = require('webpack-merge'), 3 | htmlWebpackPlugin = require('html-webpack-plugin'), 4 | cleanWebpackPlugin = require('clean-webpack-plugin'), 5 | devMode = process.env.build === 'dev', 6 | multiBuildMode = process.env.build === 'multi', 7 | legacyMode = process.env.legacy === 'true', 8 | legacyConfig = require('./legacyConfig'); 9 | 10 | const template = multiBuildMode ? require.resolve('html-webpack-multi-build-plugin/template.ejs') : require.resolve('html-webpack-plugin/default_index.ejs'); 11 | 12 | const base = { 13 | target: 'web', 14 | entry: { 15 | app: './src/index.tsx', 16 | }, 17 | output: { 18 | path: path.resolve(__dirname, '../dist'), 19 | filename: '[name].js', 20 | }, 21 | resolve: { 22 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 23 | }, 24 | plugins: [ 25 | new htmlWebpackPlugin( 26 | { 27 | inject: !multiBuildMode, 28 | template 29 | } 30 | ) 31 | ] 32 | }; 33 | 34 | 35 | if (!devMode) { 36 | base.plugins.push( 37 | new cleanWebpackPlugin(['dist'], { root: __dirname + '/..' }) 38 | ); 39 | } 40 | 41 | if (legacyMode) { 42 | module.exports = merge(base, legacyConfig); 43 | } else { 44 | module.exports = base; 45 | } 46 | -------------------------------------------------------------------------------- /example/webpack/dev.js: -------------------------------------------------------------------------------- 1 | const base = require('./base'), 2 | merge = require('webpack-merge'), 3 | webpack = require('webpack'), 4 | forkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'), 5 | legacyMode = process.env.legacy === 'true'; 6 | 7 | const devConfig = { 8 | mode: 'development', 9 | devtool: 'inline-source-map', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.tsx?$/, 14 | use: [ 15 | { 16 | loader: 'babel-loader', 17 | options: { 18 | babelrc: true, 19 | plugins: ['react-hot-loader/babel'], 20 | }, 21 | }, 22 | { 23 | loader: 'ts-loader', 24 | options: { 25 | transpileOnly: true, 26 | configFile: legacyMode ? 'tsconfig.legacy.json' : 'tsconfig.json', 27 | }, 28 | }, 29 | ], 30 | }, 31 | ], 32 | }, 33 | plugins: [ 34 | new forkTsCheckerWebpackPlugin(), 35 | new webpack.HotModuleReplacementPlugin(), 36 | ], 37 | devServer: { 38 | hot: true, 39 | host: '0.0.0.0', 40 | port: 8080, 41 | historyApiFallback: true, 42 | https: false, 43 | }, 44 | }; 45 | 46 | module.exports = merge(base, devConfig); 47 | -------------------------------------------------------------------------------- /example/webpack/legacyConfig.js: -------------------------------------------------------------------------------- 1 | const path = require('path'), 2 | packageJson = require(path.resolve(process.cwd(), 'package.json')), 3 | packageName = packageJson.name.replace(/@/, '-').replace(/\//, '-'), 4 | devMode = process.env.build === 'dev'; 5 | 6 | const legacyConfig = { 7 | name: 'LegacyConfig', 8 | entry: { 9 | app: ['babel-polyfill', './src/index.tsx'], 10 | }, 11 | output: { 12 | filename: '[name]_legacy.js' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | use: [ 19 | { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: [ 23 | [ 24 | 'env', 25 | { 26 | modules: false, 27 | useBuiltIns: true, 28 | targets: { 29 | browsers: ['> 1%', 'last 2 versions', 'Firefox ESR', 'ie >= 11'], 30 | }, 31 | }, 32 | ], 33 | ], 34 | }, 35 | }, 36 | ], 37 | }, 38 | ], 39 | }, 40 | }; 41 | 42 | module.exports = legacyConfig; 43 | -------------------------------------------------------------------------------- /example/webpack/multi.js: -------------------------------------------------------------------------------- 1 | module.exports = [require('./prod'), require('./prod.legacy')]; 2 | -------------------------------------------------------------------------------- /example/webpack/prod.js: -------------------------------------------------------------------------------- 1 | const base = require('./base'), 2 | merge = require('webpack-merge'), 3 | prodMode = process.env.build === 'prod', 4 | htmlWebpackMultiBuildPlugin = require('html-webpack-multi-build-plugin'); 5 | 6 | const prodConfig = { 7 | name: 'ProdConfig', 8 | mode: 'production', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | use: [ 14 | { 15 | loader: 'ts-loader', 16 | options: { 17 | configFile: prodMode ? 'tsconfig.json' : 'tsconfig.legacy.json', 18 | }, 19 | }, 20 | ], 21 | }, 22 | ], 23 | }, 24 | plugins: [ 25 | new htmlWebpackMultiBuildPlugin() 26 | ], 27 | optimization: { 28 | splitChunks: { 29 | chunks: 'all', 30 | }, 31 | }, 32 | }; 33 | 34 | module.exports = merge(base, prodConfig); 35 | -------------------------------------------------------------------------------- /example/webpack/prod.legacy.js: -------------------------------------------------------------------------------- 1 | const prod = require('./prod'), 2 | merge = require('webpack-merge'), 3 | legacyConfig = require('./legacyConfig'); 4 | 5 | module.exports = merge(prod, legacyConfig); 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let js = []; 3 | let run = 0; 4 | let outputFileNameRegex = []; 5 | 6 | function HtmlWebpackMultiBuildPlugin(options) { 7 | this.options = options; 8 | } 9 | 10 | HtmlWebpackMultiBuildPlugin.prototype = { 11 | apply: function(compiler) { 12 | this.createOutputRegexes(compiler.options); 13 | 14 | if (compiler.hooks) { 15 | // webpack 4 support 16 | compiler.hooks.compilation.tap( 17 | "HtmlWebpackMultiBuildPlugin", 18 | compilation => { 19 | if (compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration) { 20 | compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tapAsync( 21 | "HtmlWebpackMultiBuildPlugin", 22 | this.beforeHtmlGeneration.bind(this) 23 | ); 24 | } else { 25 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 26 | var hooks = HtmlWebpackPlugin.getHooks(compilation); 27 | hooks.beforeAssetTagGeneration.tapAsync( 28 | "HtmlWebpackMultiBuildPlugin", 29 | this.beforeHtmlGeneration.bind(this) 30 | ); 31 | } 32 | } 33 | ); 34 | } else { 35 | compiler.plugin("compilation", compilation => { 36 | compilation.plugin( 37 | "html-webpack-plugin-before-html-generation", 38 | this.beforeHtmlGeneration.bind(this) 39 | ); 40 | }); 41 | } 42 | }, 43 | beforeHtmlGeneration: function(data, cb) { 44 | this.clearOldScripts(data); 45 | ++run; 46 | js = js.concat(data.assets.js); 47 | data.assets.js = js; 48 | if (run === 2) { 49 | data.plugin.options.modernScripts = js.filter( 50 | value => value.indexOf("legacy") === -1 51 | ); 52 | data.plugin.options.legacyScripts = js.filter( 53 | value => value.indexOf("legacy") > 0 54 | ); 55 | } 56 | 57 | cb(null, data); 58 | }, 59 | createOutputRegexes: function(options) { 60 | if (options.output && options.output.filename) { 61 | // default webpack entry 62 | let entry = ["main"]; 63 | if (options.entry) { 64 | // when object is provided we have custom entry names 65 | if (typeof options.entry === "object") { 66 | entry = Object.keys(options.entry); 67 | } 68 | } 69 | entry.forEach(e => { 70 | const outFilePathForEntry = options.output.filename.replace( 71 | "[name]", 72 | e 73 | ); 74 | const matches = outFilePathForEntry.match(/\[hash(:\d+)?]/); 75 | if (matches) { 76 | // max hash length is 20 characters so limit the regex to 20 77 | const hashLength = matches[1] ? +matches[1].substr(1) : 20; 78 | outputFileNameRegex.push( 79 | new RegExp( 80 | outFilePathForEntry.replace( 81 | matches[0], 82 | `[\\w\\d]{${Math.min(hashLength, 20)}}` 83 | ) 84 | ) 85 | ); 86 | } 87 | }); 88 | } 89 | }, 90 | clearOldScripts: function(data) { 91 | outputFileNameRegex.forEach(r => { 92 | data.assets.js.forEach(a => { 93 | // we have one of our entries 94 | if (r.test(a)) { 95 | js = js.filter(j => !r.test(j)); 96 | } 97 | }); 98 | }); 99 | } 100 | }; 101 | 102 | module.exports = HtmlWebpackMultiBuildPlugin; 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-webpack-multi-build-plugin", 3 | "version": "1.1.0", 4 | "description": "simplifies creation of HTML file for multi-build configuration", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/firsttris/html-webpack-multi-build-plugin.git" 9 | }, 10 | "keywords": [ 11 | "webpack", 12 | "plugin", 13 | "html-webpack-plugin", 14 | "multi-build", 15 | "module", 16 | "nomodule", 17 | "template" 18 | ], 19 | "author": "Tristan Teufel (https://github.com/firsttris)", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/firsttris/html-webpack-multi-build-plugin/issues" 23 | }, 24 | "homepage": "https://github.com/firsttris/html-webpack-multi-build-plugin#readme" 25 | } 26 | -------------------------------------------------------------------------------- /template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.title %> 5 | <% if (Array.isArray(htmlWebpackPlugin.options.meta)) { %> 6 | <% for (item of htmlWebpackPlugin.options.meta) { %> 7 | <%= key %>="<%= item[key] %>"<% } %>> 8 | <% } %> 9 | <% } %> 10 | <% for (key in htmlWebpackPlugin.files.css) { %> 11 | 12 | <% } %> 13 | 14 | 15 | 16 | 24 | 25 | 33 | 34 | 35 | --------------------------------------------------------------------------------