├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── spec ├── index.spec.ts └── test_data │ ├── entry.js │ ├── index.html │ └── polyfill.js ├── src └── plugin.ts └── tsconfig.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build-plugin 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | webpack: [4] 9 | html-webpack: [4, 3] 10 | include: 11 | - webpack: 5.74.0 12 | html-webpack: 5 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm install 19 | - run: npm install -D webpack@${{ matrix.webpack }} html-webpack-plugin@${{ matrix.html-webpack }} 20 | - run: npm run test 21 | - run: npm run build 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy-plugin 2 | on: 3 | release: 4 | types: [released] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | webpack: [4] 11 | html-webpack: [4, 3] 12 | include: 13 | - webpack: 5.74.0 14 | html-webpack: 5 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 16 20 | - run: npm install 21 | - run: npm install -D webpack@${{ matrix.webpack }} html-webpack-plugin@${{ matrix.html-webpack }} 22 | - run: npm run test 23 | - run: npm run build 24 | deploy: 25 | needs: build 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: 16 32 | - run: npm install 33 | - run: npm install -D webpack@5.74.0 html-webpack-plugin@5 34 | - uses: JS-DevTools/npm-publish@v1 35 | with: 36 | token: ${{ secrets.NPM_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | 4 | spec/test_dist 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | spec/ 3 | node_modules/ 4 | .vscode/ 5 | .gitignore 6 | npm-debug.log 7 | tsconfig.json 8 | .travis.yml 9 | .github 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "12" 6 | 7 | env: 8 | - WEBPACK_VERSION=4 HTML_PLUGIN_VERSION=3 9 | - WEBPACK_VERSION=4 HTML_PLUGIN_VERSION=4 10 | - WEBPACK_VERSION=4 HTML_PLUGIN_VERSION=5 11 | - WEBPACK_VERSION=5 HTML_PLUGIN_VERSION=5 12 | 13 | install: 14 | - npm install 15 | - npm install -D webpack@$WEBPACK_VERSION || true 16 | - npm install -D html-webpack-plugin@$HTML_PLUGIN_VERSION || true 17 | 18 | script: 19 | - npm test 20 | - npm run build 21 | 22 | deploy: 23 | skip_cleanup: true 24 | on: 25 | repo: swimmadude66/webpack-nomodule-plugin 26 | node: "10" 27 | tags: true 28 | branch: master 29 | if: env(WEBPACK_VERSION)=4 AND env(HTML_PLUGIN_VERSION)=3 30 | provider: npm 31 | email: "swimmadude66@gmail.com" 32 | api_key: 33 | secure: "KLe0spEfJBgExBmnB4fAo6L8s9CclKFKU8rcnsNhbuGsJdqowkmcj4ahNf5lAm94Acral+ui2Ir+McAvfi5R6KIysNFnhEtMfMTQQ+1IwhRD6AfOGdEsjWFuLb9yVwAVSq5NSxa/G/fIW+0Zp/bWWiltURLjsWwJhtBbaij4/16Chu5VHZ5LX2TU5F1TJCjJqNQfh2pZH6Xh05j+FvaIaC0WIre3ZKpm0mdJJY241+dzynXw3ctVSHopR2WvzVyXbtR+/M/S4NfrgGwk0RV8KHfGjGbIxwpfuzbTvcPfQkbso6Y6ysy6EwWXbPt9qIQP7okVU8nSI7rq5m3pKlq94dKENkeEGCcOzBvKzmNHwsLPIYBbwhkQZ4ELe1OOFe+9mLih6QU4e+/nALsORF28vTFd4b5jzJq0i/Fpx6EleKqh0yeL9rVuD4KDAEdVDyqGMzIBkleS3nJE+Hs+7EQmL6UXuKUh1ttpHMNQ8zPaDmo4lMy8MYM3kKoUUxKxZ8GcjdgDJSqLMg2oR2KDu1HItXl/M4JzbjzRAf1+T3jwprpJgIJoq4nHLQHFpgH26T3Ue4UCCztHe6NIE0qD+OId8fI+QTUo3Ju+sng/oR4NUdTaW7SSrrB4ts2g5CX/2cuGBv2yWTjcfhjtDLLcYAnONJS5mhO7aAGi4F+3JCJArms=" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adam Yost 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webpack Nomodule Plugin 2 | _Assigns the `nomodule` attribute to script tags injected by [Html Webpack Plugin](https://github.com/jantimon/html-webpack-plugin)_ 3 | 4 | ![build-plugin](https://github.com/swimmadude66/webpack-nomodule-plugin/workflows/build-plugin/badge.svg?branch=master) 5 | 6 | [![NPM](https://nodei.co/npm/webpack-nomodule-plugin.png?compact=true)](https://npmjs.org/package/webpack-nomodule-plugin) 7 | ## Configuration 8 | 9 | 1. Install via `npm i -D webpack-nomodule-plugin` 10 | 1. Add to your webpack config AFTER HtmlWebpackPlugin 11 | ```javascript 12 | var NoModulePlugin = require('webpack-nomodule-plugin').WebpackNoModulePlugin; 13 | // OR for import style 14 | import {WebpackNoModulePlugin} from 'webpack-nomodule-plugin' 15 | ... 16 | plugins: [ 17 | new HtmlWebpackPlugin({ 18 | filename: join(OUTPUT_DIR, './dist/index.html'), 19 | hash: false, 20 | inject: 'body', 21 | minify: minifyOptions, 22 | showErrors: false 23 | template: join(__dirname, './src/index.html'), 24 | }), 25 | new WebpackNoModulePlugin({ 26 | filePatterns: ['polyfill.**.js'] 27 | }) 28 | ] 29 | ``` 30 | 31 | The plugin takes a configuration argument with a key called `filePatterns`. This is an array of file globs (provided via [minimatch](https://github.com/isaacs/minimatch)) representing which injected script tags to flag as nomodule. **Scripts with this attribute will not be executed on newer browsers, so IE and other browser polyfills can be skipped if not needed.** 32 | 33 | ### filePatterns 34 | The match logic will attempt to match the `src` attribute that is added to the html against each glob in the `filePatterns` config. This means if your output js is not in the same folder as your output html, you will need to specify a glob which accounts for the path from `index.html` to the output file. 35 | 36 | e.g. For a situation in which js files are output in `dist/js/..min.js` and the html is output at `dist/index.html` 37 | ```javascript 38 | plugins: [ 39 | new HtmlWebpackPlugin({ 40 | filename: join(OUTPUT_DIR, './dist/index.html'), 41 | hash: false, 42 | inject: 'body', 43 | minify: minifyOptions, 44 | showErrors: false 45 | template: join(__dirname, './src/index.html'), 46 | }), 47 | new WebpackNoModulePlugin({ 48 | filePatterns: ['js/polyfill.**.js'] 49 | // OR filePatterns: ['**/polyfill.**.js'] if the path is not known 50 | }) 51 | ] 52 | ``` 53 | 54 | ## Testing 55 | Testing is done via ts-node and mocha. Test files can be found in `/spec`, and will be auto-discovered as long as the file ends in `.spec.ts`. Just run `npm test` after installing to see the tests run. 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-nomodule-plugin", 3 | "version": "1.2.1", 4 | "description": "a plugin for html-webpack-plugin to label certain chunks as legacy-only via the nomodule attribute", 5 | "main": "dist/plugin.js", 6 | "scripts": { 7 | "test": "mocha -r ts-node/register spec/**/*.spec.ts --exit", 8 | "prebuild": "rimraf dist", 9 | "build": "tsc", 10 | "prepublishOnly": "npm run build" 11 | }, 12 | "keywords": [ 13 | "html-webpack-plugin", 14 | "nomodule", 15 | "polyfills", 16 | "webpack", 17 | "plugin" 18 | ], 19 | "author": "swimmadude66", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@types/chai": "4.2.14", 23 | "@types/mocha": "8.2.0", 24 | "@types/node": "12.19.15", 25 | "chai": "4.2.0", 26 | "html-webpack-plugin": ">=3.2.0", 27 | "mocha": "8.2.1", 28 | "rimraf": "3.0.2", 29 | "ts-node": "9.1.1", 30 | "typescript": "4.1.3", 31 | "webpack": ">=3.0.0" 32 | }, 33 | "dependencies": { 34 | "minimatch": "~3.0.4" 35 | }, 36 | "peerDependencies": { 37 | "html-webpack-plugin": ">=3.0.0", 38 | "webpack": ">=3.0.0" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/swimmadude66/webpack-nomodule-plugin.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/swimmadude66/webpack-nomodule-plugin/issues" 46 | }, 47 | "homepage": "https://github.com/swimmadude66/webpack-nomodule-plugin#readme" 48 | } 49 | -------------------------------------------------------------------------------- /spec/index.spec.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {readFileSync} from 'fs'; 3 | import {expect} from 'chai'; 4 | import * as webpack from 'webpack'; 5 | import * as HtmlWebpackPlugin from 'html-webpack-plugin'; 6 | import * as rimraf from 'rimraf'; 7 | import {WebpackNoModulePlugin} from '../src/plugin'; 8 | 9 | const OUTPUT_DIR = join(__dirname, './test_dist'); 10 | 11 | const HtmlWebpackPluginOptions = { 12 | filename: 'index.html', 13 | hash: false, 14 | inject: 'body' as 'body', 15 | minify: { 16 | collapseWhitespace: true, 17 | removeComments: true, 18 | removeRedundantAttributes: true, 19 | useShortDoctype: true, 20 | 21 | }, 22 | showErrors: true, 23 | template: join(__dirname, './test_data/index.html'), 24 | }; 25 | 26 | const webpackDevOptions: webpack.Configuration = { 27 | mode: 'development', 28 | entry: { 29 | app: join(__dirname, './test_data/entry.js'), 30 | polyfill: join(__dirname, './test_data/polyfill.js'), 31 | }, 32 | output: { 33 | path: OUTPUT_DIR 34 | } 35 | }; 36 | 37 | const webpackProdOptions: webpack.Configuration = { 38 | ...webpackDevOptions, 39 | output: { 40 | filename: '[name].[contenthash].min.js', 41 | path: OUTPUT_DIR, 42 | pathinfo: true 43 | }, 44 | mode: 'production', 45 | }; 46 | 47 | function getOutput(): string { 48 | const htmlFile = join(OUTPUT_DIR, './index.html'); 49 | const htmlContents = readFileSync(htmlFile).toString('utf8'); 50 | expect(!!htmlContents).to.be.true; 51 | return htmlContents; 52 | } 53 | 54 | console.log('\nWEBPACK VERSION', webpack.version,'\n'); 55 | console.log('\nHTML-WEBPACK_PLUGIN VERSION', HtmlWebpackPlugin.version,'\n'); 56 | 57 | describe('WebpackNoModulePlugin Development Mode', () => { 58 | 59 | afterEach((done) => { 60 | rimraf(OUTPUT_DIR, done); 61 | }); 62 | 63 | it('should do nothing when no patterns are specified', function (done) { 64 | webpack({ ...webpackDevOptions, 65 | plugins: [ 66 | new HtmlWebpackPlugin(HtmlWebpackPluginOptions), 67 | new WebpackNoModulePlugin(), 68 | ] 69 | }, (err) => { 70 | expect(!!err).to.be.false; 71 | const html = getOutput(); 72 | expect(/script\s+.*?src\s*=\s*"(\/)?polyfill\.js"/i.test(html), 'could not find polyfill bundle').to.be.true; 73 | expect(/script\s+.*?src\s*=\s*"(\/)?app\.js"/i.test(html), 'could not find app bundle').to.be.true; 74 | done(); 75 | }); 76 | }); 77 | 78 | it('should mark script as nomodule if the pattern matches', function (done) { 79 | webpack({ ...webpackDevOptions, 80 | plugins: [ 81 | new HtmlWebpackPlugin(HtmlWebpackPluginOptions), 82 | new WebpackNoModulePlugin({ 83 | filePatterns: ['polyfill\.js'] 84 | }), 85 | ] 86 | }, (err) => { 87 | expect(!!err).to.be.false; 88 | const html = getOutput(); 89 | expect(/script\s+.*src\s*=\s*"(\/)?polyfill\.js"/i.test(html), 'could not find polyfill bundle').to.be.true; 90 | expect(/script\s+.*src\s*=\s*"(\/)?app\.js"/i.test(html), 'could not find app bundle').to.be.true; 91 | expect(/script\s+.*?src\s*=\s*"(\/)?polyfill\.js"\s+nomodule/i.test(html), 'attribute missing from polyfill bundle').to.be.true; 92 | expect(/script\s+.*?src\s*=\s*"(\/)?app\.js"\s+nomodule/i.test(html), 'attribute present on app bundle').to.be.false; 93 | done(); 94 | }); 95 | }); 96 | }); 97 | 98 | 99 | describe('WebpackNoModulePlugin Production Mode', () => { 100 | 101 | afterEach((done) => { 102 | rimraf(OUTPUT_DIR, done); 103 | }); 104 | 105 | it('should do nothing when no patterns are specified', function (done) { 106 | webpack({ ...webpackProdOptions, 107 | plugins: [ 108 | new HtmlWebpackPlugin(HtmlWebpackPluginOptions), 109 | new WebpackNoModulePlugin(), 110 | ] 111 | }, (err) => { 112 | expect(!!err).to.be.false; 113 | const html = getOutput(); 114 | expect(/script\s+.*?src\s*=\s*"(\/)?polyfill\.[a-z0-9]+\.min\.js"/i.test(html), 'could not find polyfill bundle').to.be.true; 115 | expect(/script\s+.*?src\s*=\s*"(\/)?app\.[a-z0-9]+\.min\.js"/i.test(html), 'could not find app bundle').to.be.true; 116 | done(); 117 | }); 118 | }); 119 | 120 | it('should mark script as nomodule if the pattern matches', function (done) { 121 | webpack({ ...webpackProdOptions, 122 | plugins: [ 123 | new HtmlWebpackPlugin(HtmlWebpackPluginOptions), 124 | new WebpackNoModulePlugin({ 125 | filePatterns: ['polyfill.**.js'] 126 | }), 127 | ] 128 | }, (err) => { 129 | expect(!!err).to.be.false; 130 | const html = getOutput(); 131 | expect(/<\s*script\s+.*src\s*=\s*"(\/)?polyfill\.[a-z0-9]+\.min\.js"/i.test(html), 'could not find polyfill bundle').to.be.true; 132 | expect(/<\s*script\s+.*src\s*=\s*"(\/)?app\.[a-z0-9]+\.min\.js"/i.test(html), 'could not find app bundle').to.be.true; 133 | expect(/<\s*script\s+.*?src\s*=\s*"(\/)?polyfill\.[a-z0-9]+\.min\.js"\s+nomodule/i.test(html), 'attribute missing from polyfill bundle').to.be.true; 134 | expect(/<\s*script\s+.*?src\s*=\s*"(\/)?app\.[a-z0-9]+\.min\.js"\s+nomodule/i.test(html), 'attribute present on app bundle').to.be.false; 135 | done(); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /spec/test_data/entry.js: -------------------------------------------------------------------------------- 1 | console.log('hello there, general webpacky'); -------------------------------------------------------------------------------- /spec/test_data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test Plugin 4 | 5 | 6 |
Content goes here
7 | 8 | -------------------------------------------------------------------------------- /spec/test_data/polyfill.js: -------------------------------------------------------------------------------- 1 | console.log('I only run on IE :('); -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import * as HtmlWebpackPlugin from 'html-webpack-plugin'; 2 | import * as minimatch from 'minimatch'; 3 | 4 | export interface NoModuleConfig { 5 | filePatterns: string[]; 6 | // in case I want to add other optional configs later without breaking old uses 7 | } 8 | 9 | export class WebpackNoModulePlugin { 10 | 11 | constructor(private _config: NoModuleConfig = {filePatterns: []}) { 12 | 13 | } 14 | 15 | apply(compiler) { 16 | if (compiler.hooks) { 17 | // webpack 4 support 18 | compiler.hooks.compilation.tap('NoModulePlugin', (compilation) => { 19 | if (compilation.hooks.htmlWebpackPluginAlterAssetTags) { 20 | // html webpack 3 21 | compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync( 22 | 'NoModulePlugin', 23 | (data, cb) => { 24 | data.head = this._transformAssets(data.head); 25 | data.body = this._transformAssets(data.body); 26 | return cb(null, data); 27 | } 28 | ) 29 | } else if (HtmlWebpackPlugin && HtmlWebpackPlugin.getHooks) { 30 | // html-webpack 4 31 | const hooks = HtmlWebpackPlugin.getHooks(compilation); 32 | hooks.alterAssetTags.tapAsync( 33 | 'NoModulePlugin', 34 | (data, cb) => { 35 | data.assetTags.scripts = this._transformAssets(data.assetTags.scripts); 36 | data.assetTags.styles = this._transformAssets(data.assetTags.styles); 37 | data.assetTags.meta = this._transformAssets(data.assetTags.meta); 38 | return cb(null, data); 39 | } 40 | ) 41 | } else { 42 | throw new Error('Cannot find appropriate compilation hook'); 43 | } 44 | }); 45 | } else { 46 | // Hook into the html-webpack-plugin processing 47 | compiler.plugin('compilation', function (compilation) { 48 | compilation.plugin('html-webpack-plugin-alter-asset-tags', (htmlPluginData, callback) => { 49 | htmlPluginData.head = this._transformAssets(htmlPluginData.head); 50 | htmlPluginData.body = this._transformAssets(htmlPluginData.body); 51 | return callback(null, htmlPluginData); 52 | }); 53 | }); 54 | } 55 | } 56 | 57 | private _transformAssets(assets: any[]): any[] { 58 | return assets.map(s => { 59 | if (s.tagName && s.tagName === 'script' && s.attributes && s.attributes.src) { 60 | const nomodule = this._config.filePatterns.some(pattern => minimatch(s.attributes.src, pattern)); 61 | if (nomodule) { 62 | s.attributes.nomodule = true; 63 | if (s.attributes.type === 'module') { 64 | delete s.attributes.type; 65 | } 66 | } 67 | } 68 | return s; 69 | }); 70 | } 71 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": true, 10 | "noImplicitAny": false, 11 | "skipLibCheck": true, 12 | "outDir": "dist", 13 | "lib": [ 14 | "es6" 15 | ], 16 | "types": [ 17 | "node", 18 | "mocha", 19 | "chai" 20 | ] 21 | }, 22 | "exclude": [ 23 | "node_modules/", 24 | "spec/", 25 | "**/*.spec.js", 26 | "**/*.spec.ts", 27 | ] 28 | } 29 | --------------------------------------------------------------------------------