├── .editorconfig ├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.V1.md ├── README.md ├── index.js ├── package.json ├── spec ├── api.spec.js ├── browser.spec.js ├── end_to_end.spec.js ├── fixtures │ ├── a-dir │ │ ├── file-a.css │ │ ├── file-a.js │ │ ├── file-b.css │ │ └── file-b.js │ ├── app.css │ ├── astyle.css │ ├── entry.js │ ├── exclude.css │ ├── external │ │ ├── external-entry.js │ │ ├── external-style.css │ │ ├── index.html │ │ ├── node_modules │ │ │ ├── fake-a-package │ │ │ │ ├── fake-a-bundle.js │ │ │ │ ├── fake-a-entry.js │ │ │ │ └── package.json │ │ │ ├── fake-b-package │ │ │ │ ├── fake-b-bundle.js │ │ │ │ ├── fake-b-entry.js │ │ │ │ └── package.json │ │ │ ├── fake-c-package │ │ │ │ ├── fake-c-bundle.js │ │ │ │ ├── fake-c-entry.js │ │ │ │ └── package.json │ │ │ └── fake-other-package │ │ │ │ ├── fake-other-bundle.js │ │ │ │ └── package.json │ │ └── package.json │ ├── glob-a.css │ ├── glob-a.js │ ├── glob-b.css │ ├── glob-b.js │ ├── glob.css │ ├── glob.js │ ├── index-no-inject.html │ ├── index.html │ └── other ├── option_validation.spec.js └── support │ └── jasmine.json ├── typings.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | /dist/ 4 | npm-debug.log 5 | /.idea/ 6 | *.iml 7 | spec/**/dist 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "12" 5 | - "14" 6 | script: 7 | - npm test -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Yarn Test", 11 | "program": "${workspaceFolder}/node_modules/.bin/jasmine" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | https://github.com/jharris4/html-webpack-tags-plugin 5 | 6 | 7 | # [3.0.2](https://github.com/jharris4/html-webpack-tags-plugin/compare/3.0.1...3.0.2) (2021-10-30) 8 | 9 | ### Bug Fixes 10 | 11 | * Fix script attributes not being copied when scripts are injected in the head of the html document [[#79](https://github.com/jharris4/html-webpack-tags-plugin/issues/79)]. 12 | 13 | 14 | # [3.0.1](https://github.com/jharris4/html-webpack-tags-plugin/compare/3.0.0...3.0.1) (2021-04-07) 15 | 16 | ### Features 17 | 18 | * Added `webpack` & `html-webpack-plugin` to peerDependencies. 19 | 20 | 21 | # [3.0.0](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.17...3.0.0) (2021-02-03) 22 | 23 | ### Features 24 | 25 | * Updated to support `webpack` & `html-webpack-plugin` version **`5`**. 26 | 27 | ### BREAKING CHANGES 28 | 29 | * webpack` & `html-webpack-plugin` version **`5`** are now required. 30 | * Node version **`>=10`** is now required. 31 | 32 | 33 | 34 | # [2.0.17](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.16...2.0.17) (2019-09-23) 35 | 36 | ### Bug Fixes 37 | 38 | * Add typings.d.ts to `files` in `package.json` (oops) [[#52](https://github.com/jharris4/html-webpack-tags-plugin/issues/52)]. 39 | 40 | 41 | # [2.0.16](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.15...2.0.16) (2019-09-23) 42 | 43 | ### Features 44 | 45 | * Add TypeScript definitions [[#52](https://github.com/jharris4/html-webpack-tags-plugin/issues/52)]. 46 | 47 | 48 | # [2.0.15](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.14...2.0.15) (2019-08-20) 49 | 50 | ### Features 51 | 52 | * Renamed the meta option to **`metas`**. The plural version is more consistent with the **`tags`**, **`scripts`** and **`links`** options. 53 | 54 | 55 | # [2.0.14](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.13...2.0.14) (2019-08-20) 56 | 57 | ### Features 58 | 59 | * Add new **`meta`** option (default **undefined**) that allows `` tags to be injected. 60 | 61 | 62 | # [2.0.13](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.12...2.0.13) (2019-06-18) 63 | 64 | ### Bug Fixes 65 | 66 | * Use `url.resolve` instead of `path.join` to fix a bug when the publicPath contains `//`. [[#47](https://github.com/jharris4/html-webpack-tags-plugin/issues/47)]. 67 | 68 | ### Features 69 | 70 | * Update all dependency packages to latest. 71 | 72 | 73 | # [2.0.12](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.11...2.0.12) (2019-05-03) 74 | 75 | ### Features 76 | 77 | * Update `slash` package to `3.0.0`. 78 | 79 | 80 | # [2.0.11](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.10...2.0.11) (2019-05-03) 81 | 82 | ### Features 83 | 84 | * Add new **`globFlatten`** tag option (default **false**) that allows paths to be stripped from glob matched file paths. 85 | 86 | 87 | # [2.0.10](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.9...2.0.10) (2019-04-27) 88 | 89 | ### Features 90 | 91 | * Add new **`prependExternals`** option (default **true**) that auto-prepends (**`append`**: false) any scripts with the **`external`** option specified. 92 | 93 | 94 | # [2.0.9](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.8...2.0.9) (2019-04-24) 95 | 96 | ### Bug Fixes 97 | 98 | * More robust validation logic for all options across the board. 99 | 100 | ### Features 101 | 102 | * Adds support for all `top` level options to be specified at the `tag` level. 103 | * `HtmlWebpackTagsPlugin.api` now ready for use by any plugins wanting to extend this plugin's options. 104 | 105 | 106 | # [2.0.8](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.7...2.0.8) (2019-04-23) 107 | 108 | ### Features 109 | 110 | * Adds an `api` property to the plugin constructor, allowing reuse of option validation by other plugins. 111 | 112 | 113 | # [2.0.7](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.6...2.0.7) (2019-04-23) 114 | 115 | ### Features 116 | 117 | * Adds stricter/better option validation. 118 | 119 | 120 | # [2.0.6](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.5...2.0.6) (2019-04-23) 121 | 122 | ### Bug Fixes 123 | 124 | * Fixes use of this plugin with [html-webpack-plugin@4.x](https://github.com/jantimon/html-webpack-plugin). [[#45](https://github.com/jharris4/html-webpack-tags-plugin/issues/45)]. 125 | 126 | 127 | # [2.0.5](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.4...2.0.5) (2019-04-23) 128 | 129 | ### Features 130 | 131 | * Adds support for specifying the append option at the tag level. 132 | 133 | 134 | # [2.0.4](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.3...2.0.4) (2019-04-22) 135 | 136 | ### Features 137 | 138 | * Added browser tests to this package (using [puppeteer](https://github.com/GoogleChrome/puppeteer)). 139 | 140 | ### Bug Fixes 141 | 142 | * Fix **windows** `path` formatting issues introduced in the `2.x` version rewrite. [[#44](https://github.com/jharris4/html-webpack-tags-plugin/issues/44)]. 143 | 144 | 145 | # [2.0.3](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.2...2.0.3) (2019-04-19) 146 | 147 | ### Features 148 | 149 | * Adds support for new **external** script tag options that can control webpack's external config. 150 | 151 | 152 | # [2.0.2](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.1...2.0.2) (2019-04-19) 153 | 154 | ### Bug Fixes 155 | 156 | * Fix bugs related to renaming the **assets** option name to **tags**. 157 | 158 | 159 | # [2.0.1](https://github.com/jharris4/html-webpack-tags-plugin/compare/2.0.0...2.0.1) (2019-04-19) 160 | 161 | Version `2.0.1` renamed this package from `html-webpack-include-assets-plugin` to `html-webpack-tags-plugin`. 162 | 163 | ### BREAKING CHANGES 164 | 165 | * The **assets** option was renamed to the **tags** option 166 | * The **asset.assetPath** option was renamed to **asset.sourcePath** 167 | 168 | 169 | # [2.0.0](https://github.com/jharris4/html-webpack-tags-plugin/compare/1.0.10...2.0.0) (2019-04-18) 170 | 171 | Version `2.0.0` is a full rewrite of this plugin using ES6 instead of ES5 source code. 172 | 173 | ### Bug Fixes 174 | 175 | * More robust logic for separating `script` vs `link` tags compared with version `1.0.x 176 | * More robust logic for injecting attributes into `link` tags 177 | * Fix inconsistencies with the `hash` and `publicPath` options from version `1.0.x` 178 | 179 | 180 | ### Features 181 | 182 | * New `links` and `scripts` plugin options added as shortcuts for injecting `assets` without worrying about `type` or `file extension` 183 | 184 | 185 | ### BREAKING CHANGES 186 | 187 | * **Node >= 8.6** is now required due to the use of `object spread` syntax in the plugin source code 188 | * **append** option now defaults to **true** 189 | 190 | 191 | 192 | # [1.0.10](https://github.com/jharris4/html-webpack-tags-plugin/compare/1.0.9...1.0.10) (2018-04-12) 193 | 194 | This is the last `1.0.x` version which supports **Node < 8.6**. 195 | 196 | * Rename links to cssAssets and improve test coverage ([7e78bec](https://github.com/jharris4/html-webpack-tags-plugin/commit/7e78bec)) 197 | * Add selfClosingTag and voidTag to links ([97ac502](https://github.com/jharris4/html-webpack-tags-plugin/commit/97ac502)) 198 | * misc cleanup ([6ca39ac](https://github.com/jharris4/html-webpack-tags-plugin/commit/6ca39ac)) 199 | 200 | 201 | # [1.0.9](https://github.com/jharris4/html-webpack-tags-plugin/compare/1.0.8...1.0.9) (2018-04-12) 202 | 203 | This version adds support for the `links` option, similar to the option in version `2.x` except `href` is used instead of `path`. 204 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jon Harris 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.V1.md: -------------------------------------------------------------------------------- 1 | Include Assets extension for the HTML Webpack Plugin 2 | ======================================== 3 | [![npm version](https://badge.fury.io/js/html-webpack-include-assets-plugin.svg)](https://badge.fury.io/js/html-webpack-include-assets-plugin) [![Build Status](https://travis-ci.org/jharris4/html-webpack-include-assets-plugin.svg?branch=master)](https://travis-ci.org/jharris4/html-webpack-include-assets-plugin) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/Flet/semistandard) 4 | 5 | This is the `README.md` from **version 1.x** which provides support for ** Node < 8.6 **. 6 | 7 | This **version is deprecated** in favour of [https://github.com/jharris4/html-webpack-include-assets-plugin](html-webpack-include-assets-plugin version 2). 8 | 9 | Enhances [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin) 10 | functionality by allowing you to specify js or css assets to be included. 11 | 12 | When using a plugin such as [copy-webpack-plugin](https://github.com/webpack-contrib/copy-webpack-plugin) you may have assets output to your build directory that are not detected/output by the html-webpack-plugin. 13 | 14 | This plugin allows you to force some of these assets to be included in the output from html-webpack-plugin. 15 | 16 | Installation 17 | ------------ 18 | You must be running webpack on node 8.x or higher 19 | 20 | Install the plugin with npm: 21 | ```shell 22 | $ npm install --save-dev html-webpack-include-assets-plugin 23 | ``` 24 | 25 | 26 | Basic Usage 27 | ----------- 28 | Require the plugin in your webpack config: 29 | 30 | ```javascript 31 | var HtmlWebpackIncludeAssetsPlugin = require('html-webpack-include-assets-plugin'); 32 | ``` 33 | 34 | Add the plugin to your webpack config: 35 | 36 | ```javascript 37 | output: { 38 | publicPath: '/abc/' 39 | }, 40 | plugins: [ 41 | new HtmlWebpackPlugin(), 42 | new HtmlWebpackIncludeAssetsPlugin({ assets: ['a.js', 'b.css'], append: true }) 43 | ] 44 | ``` 45 | 46 | Which will generate html like this: 47 | 48 | ```html 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ``` 58 | 59 | 60 | Options 61 | ------- 62 | The available options are: 63 | 64 | - `jsExtensions`: `string` or `array` 65 | 66 | Specifies the file extensions to use to determine if assets are script assets. Default is `['.js']`. 67 | 68 | - `cssExtensions`: `string` or `array` 69 | 70 | Specifies the file extensions to use to determine if assets are style assets. Default is `['.css']`. 71 | 72 | - `assets`: `string` or `array` or `object` 73 | 74 | Assets that will be output into your html-webpack-plugin template. 75 | 76 | To specify just one asset, simply pass a string or object. To specify multiple, pass an array of strings or objects. 77 | 78 | If the asset path is static and ends in one of the `jsExtensions` or `cssExtensions` values, simply use a string value. 79 | 80 | If the asset is not static or does not have a valid extension, you can instead pass an object with properties `path` (required) and `type` or `glob` or `globPath` or `attributes` (optional). In this case `path` is the asset href/src, `type` is one of `js` or `css`, and `glob` is a wildcard to use to match all files in the path (uses the [glob](https://github.com/isaacs/node-glob) package). The `globPath` can be used to specify the directory from which the `glob` should search for filename matches (the default is to use `path` within webpack's output directory). 81 | 82 | The `attributes` property may be used to add additional attributes to the link or script element that is injected. The keys of this object are attribute names and the values are the attribute values (string or boolean key values are allowed). 83 | 84 | The `assetPath` property may be used to specify the full path to the included asset. This can be useful as it will trigger a recompilation after the assets have changed when using `webpack-dev-server` or `webpack-dev-middleware` in development mode. 85 | 86 | - `append`: `boolean` 87 | 88 | Specifying whether the assets should be prepended (`false`) before any existing assets, or appended (`true`) after them. 89 | 90 | - `resolvePaths`: `boolean` 91 | 92 | Specifying whether the asset paths should be resolved with `path.resolve` (i.e. made absolute). 93 | 94 | - `publicPath`: `boolean` or `string` 95 | 96 | Specifying whether the assets should be prepended with webpack's public path or a custom publicPath (`string`). 97 | 98 | A value of `false` may be used to disable prefixing with webpack's publicPath, or a value like `myPublicPath/` may be used to prefix all assets with the given string. Default is `true`. 99 | 100 | - `hash`: `boolean` or `function(assetName, hash)` 101 | 102 | Specifying whether the assets should be appended with webpack's compilation hash. This is useful for cache busting. Default is `false`. 103 | 104 | - `files`: `string` or `array` 105 | 106 | Files that the assets will be added to. 107 | 108 | By default the assets will be included in all files. If files are defined, the assets will only be included in specified file globs (uses the [minimatch](https://github.com/isaacs/minimatch) package). 109 | 110 | - `cssAssets`: `array` 111 | 112 | Optional shortcut for adding css assets. An array of css asset objects. 113 | 114 | See the cssAssets example below for the syntax of css asset object. 115 | 116 | 117 | Example 118 | ------- 119 | 120 | _____ 121 | 122 | Using `HtmlWebpackIncludeAssetsPlugin` and `CopyWebpackPlugin` to include assets to `html-webpack-plugin` template : 123 | 124 | ```javascript 125 | plugins: [ 126 | new CopyWebpackPlugin([ 127 | { from: 'node_modules/bootstrap/dist/css', to: 'css/'}, 128 | { from: 'node_modules/bootstrap/dist/fonts', to: 'fonts/'} 129 | ]), 130 | new HtmlWebpackPlugin(), 131 | new HtmlWebpackIncludeAssetsPlugin({ 132 | assets: ['css/bootstrap.min.css', 'css/bootstrap-theme.min.css'], 133 | append: false 134 | }) 135 | ] 136 | ``` 137 | 138 | _____ 139 | 140 | Appending and prepending at the same time : 141 | 142 | ```javascript 143 | plugins: [ 144 | new CopyWebpackPlugin([ 145 | { from: 'node_modules/bootstrap/dist/css', to: 'css/'}, 146 | { from: 'node_modules/bootstrap/dist/fonts', to: 'fonts/'} 147 | ]), 148 | new HtmlWebpackPlugin(), 149 | new HtmlWebpackIncludeAssetsPlugin({ 150 | assets: ['css/bootstrap.min.css', 'css/bootstrap-theme.min.css'], 151 | append: false 152 | }), 153 | new HtmlWebpackIncludeAssetsPlugin({ 154 | assets: ['css/custom.css'], 155 | append: true 156 | }) 157 | ] 158 | ``` 159 | 160 | _____ 161 | 162 | Using custom `jsExtensions` : 163 | 164 | ```javascript 165 | plugins: [ 166 | new HtmlWebpackPlugin(), 167 | new HtmlWebpackIncludeAssetsPlugin({ 168 | assets: ['dist/output.js', 'lib/content.jsx'], 169 | append: false, 170 | jsExtensions: ['.js', '.jsx'] 171 | }) 172 | ] 173 | ``` 174 | 175 | _____ 176 | 177 | Using custom `publicPath` : 178 | 179 | ```javascript 180 | plugins: [ 181 | new CopyWebpackPlugin([ 182 | { from: 'node_modules/bootstrap/dist/css', to: 'css/'}, 183 | { from: 'node_modules/bootstrap/dist/fonts', to: 'fonts/'} 184 | ]), 185 | new HtmlWebpackPlugin(), 186 | new HtmlWebpackIncludeAssetsPlugin({ 187 | assets: ['css/bootstrap.min.css', 'css/bootstrap-theme.min.css'], 188 | append: false, 189 | publicPath: 'myPublicPath/' 190 | }) 191 | ] 192 | ``` 193 | 194 | _____ 195 | 196 | Or to include assets without prepending the `publicPath`: 197 | 198 | ```javascript 199 | plugins: [ 200 | new HtmlWebpackPlugin(), 201 | new HtmlWebpackIncludeAssetsPlugin({ 202 | assets: ['css/no-public-path.min.css', 'http://some.domain.com.js'], 203 | append: false, 204 | publicPath: false 205 | }) 206 | ] 207 | ``` 208 | 209 | _____ 210 | 211 | Manually specifying asset types : 212 | 213 | ```javascript 214 | plugins: [ 215 | new CopyWebpackPlugin([ 216 | { from: 'node_modules/bootstrap/dist/css', to: 'css/'}, 217 | { from: 'node_modules/bootstrap/dist/fonts', to: 'fonts/'} 218 | ]), 219 | new HtmlWebpackPlugin(), 220 | new HtmlWebpackIncludeAssetsPlugin({ 221 | assets: [ 222 | '/css/bootstrap.min.css', 223 | '/css/bootstrap-theme.min.css', 224 | { path: 'https://fonts.googleapis.com/css?family=Material+Icons', type: 'css' } 225 | ], 226 | append: false, 227 | publicPath: '' 228 | }) 229 | ] 230 | ``` 231 | 232 | _____ 233 | 234 | Adding custom attributes to asset tags : 235 | 236 | The bootstrap-theme link tag will be given an id="bootstrapTheme" attribute. 237 | 238 | ```javascript 239 | plugins: [ 240 | new CopyWebpackPlugin([ 241 | { from: 'node_modules/bootstrap/dist/css', to: 'css/'}, 242 | { from: 'node_modules/bootstrap/dist/fonts', to: 'fonts/'} 243 | ]), 244 | new HtmlWebpackPlugin(), 245 | new HtmlWebpackIncludeAssetsPlugin({ 246 | assets: [ 247 | '/css/bootstrap.min.css', 248 | { path: '/css/bootstrap-theme.min.css', attributes: { id: 'bootstrapTheme' } } 249 | ], 250 | append: false, 251 | publicPath: '' 252 | }) 253 | ] 254 | ``` 255 | 256 | _____ 257 | 258 | Using `hash` option : 259 | 260 | When the hash option is set to `true`, asset paths will be appended with a hash query parameter (`?hash=`) 261 | 262 | ```javascript 263 | plugins: [ 264 | new CopyWebpackPlugin([ 265 | { from: 'node_modules/bootstrap/dist/css', to: 'css/'}, 266 | { from: 'node_modules/bootstrap/dist/fonts', to: 'fonts/'} 267 | ]), 268 | new HtmlWebpackPlugin(), 269 | new HtmlWebpackIncludeAssetsPlugin({ 270 | assets: ['css/bootstrap.min.css', 'css/bootstrap-theme.min.css'], 271 | append: false, 272 | hash: true 273 | }) 274 | ] 275 | ``` 276 | 277 | _____ 278 | 279 | When the hash option is set to a `function`, asset paths will be replaced with the result of executing that function 280 | 281 | ```javascript 282 | plugins: [ 283 | new CopyWebpackPlugin([ 284 | { from: 'somepath/somejsfile.js', to: 'js/somejsfile.[hash].js' }, 285 | { from: 'somepath/somecssfile.css', to: 'css/somecssfile.[hash].css' } 286 | ]), 287 | new HtmlWebpackPlugin(), 288 | new HtmlWebpackIncludeAssetsPlugin({ 289 | assets: [{ path: 'js', glob: '*.js', globPath: 'somepath' }], 290 | assets: [{ path: 'css', glob: '*.css', globPath: 'somepath' }], 291 | append: false, 292 | hash: function(assetName, hash) { 293 | assetName = assetName.replace(/\.js$/, '.' + hash + '.js'); 294 | assetName = assetName.replace(/\.css$/, '.' + hash + '.css'); 295 | return assetName; 296 | } 297 | }) 298 | ] 299 | ``` 300 | 301 | _____ 302 | 303 | Specifying specific `files` 304 | 305 | ```javascript 306 | plugins: [ 307 | new CopyWebpackPlugin([ 308 | { from: 'node_modules/bootstrap/dist/css', to: 'css/'}, 309 | { from: 'node_modules/bootstrap/dist/fonts', to: 'fonts/'} 310 | ]), 311 | new HtmlWebpackPlugin({ 312 | filename: 'a/index.html' 313 | }), 314 | new HtmlWebpackPlugin({ 315 | filename: 'b/index.html' 316 | }), 317 | new HtmlWebpackIncludeAssetsPlugin({ 318 | files: ['a/**/*.html'], 319 | assets: ['css/a.css'], 320 | append: true 321 | }), 322 | new HtmlWebpackIncludeAssetsPlugin({ 323 | files: ['b/**/*.html'], 324 | assets: ['css/b.css'], 325 | append: true 326 | }) 327 | ] 328 | ``` 329 | 330 | _____ 331 | 332 | Specifying assets usings a `glob` 333 | 334 | Note that since `copy-webpack-plugin` does not actually copy the files to webpack's output directory until *after* `html-webpack-plugin` has completed, it is necessary to use the `globPath` to retrieve filename matches relative to the original location of any such files. 335 | 336 | ```javascript 337 | plugins: [ 338 | new CopyWebpackPlugin([ 339 | { from: 'node_modules/bootstrap/dist/css', to: 'css/'}, 340 | { from: 'node_modules/bootstrap/dist/fonts', to: 'fonts/'} 341 | ]), 342 | new HtmlWebpackPlugin(), 343 | new HtmlWebpackIncludeAssetsPlugin({ 344 | assets: [{ path: 'css', glob: '*.css', globPath: 'node_modules/bootstrap/dist/css/' }], 345 | append: true 346 | }) 347 | ] 348 | ``` 349 | 350 | _____ 351 | 352 | Specifying `cssAssets` (a shortcut for specifying assets of type css) 353 | 354 | ```javascript 355 | output: { 356 | publicPath: '/my-public-path/' 357 | }, 358 | plugins: [ 359 | new CopyWebpackPlugin([ 360 | { from: 'node_modules/bootstrap/dist/css', to: 'css/'}, 361 | { from: 'node_modules/bootstrap/dist/fonts', to: 'fonts/'} 362 | ]), 363 | new HtmlWebpackPlugin(), 364 | new HtmlWebpackIncludeAssetsPlugin({ 365 | assets: [], 366 | append: true, 367 | cssAssets: [ 368 | { 369 | href: 'asset/path', 370 | attributes: { 371 | rel: 'icon' 372 | } 373 | }, 374 | { 375 | href: '/absolute/asset/path', 376 | asset: false, 377 | attributes: { 378 | rel: 'manifest' 379 | } 380 | } 381 | ] 382 | }) 383 | ] 384 | ``` 385 | 386 | Will append the following link elements into the index template html 387 | 388 | ```html 389 | 390 | 391 | 392 | 393 | 394 | ``` 395 | 396 | Note that the second cssAsset's href was not prefixed with the webpack `publicPath` because `csAsset.asset` was set to `false`. 397 | 398 | _____ 399 | 400 | 401 | Caveats 402 | ------- 403 | 404 | Some users have encountered issues with plugin ordering. 405 | 406 | - It is advisable to always place any `HtmlWebpackPlugin` plugins **before** any `HtmlWebpackIncludeAssetsPlugin` plugins in your webpack config. 407 | 408 | This plugin has only been tested with **two instances** in one webpack config, where one had `option.append: false` and the other had `option.append: true`. 409 | 410 | - It is **not recommended to use more than one instance of this plugin** in one webpack config unless using the above configuration. 411 | 412 | Changing `HtmlWebpackPlugin.options.inject` from its **default value** may cause **issues**. 413 | 414 | - This plugin **requires** `HtmlWebpackPlugin.options.inject` to be `true` (it defaults to true if undefined) for attribute injection to work. 415 | 416 | 417 | If you setup your webpack config to have `HtmlWebpackPlugin.options.inject: false` like this: 418 | 419 | ```javascript 420 | output: { 421 | publicPath: '/the-public-path/' 422 | }, 423 | plugins: [ 424 | new HtmlWebpackPlugin({ inject: false }), 425 | new HtmlWebpackIncludeAssetsPlugin({ 426 | assets: [{ path: 'css/bootstrap-theme.min.css', attributes: { id: 'bootstrapTheme' } }], 427 | links: [{ href: 'the-ref', attributes: { rel: 'icon' } }], 428 | append: true 429 | }) 430 | ] 431 | ``` 432 | 433 | You will need to add the following to your *template* `index.html` to get assets to be **generated**: 434 | 435 | ```html 436 | 437 | 438 | <% for (var cssIndex = 0; cssIndex < htmlWebpackPlugin.files.css.length; cssIndex++) { %> 439 | 440 | <% } %> 441 | 442 | 443 | 444 | <% for (var jsIndex = 0; jsIndex < htmlWebpackPlugin.files.js.length; jsIndex++) { %> 445 | 446 | <% } %> 447 | 448 | ``` 449 | 450 | Using the (lodash) `template syntax` like this for css and js files is necessary when you turn injection off. 451 | 452 | But, the `template syntax` does not allow injection of more than `one attribute value`. 453 | 454 | This means it will **generate** an `index.html` that looks like this: 455 | 456 | ```html 457 | 458 | 459 | 460 | 461 | ``` 462 | 463 | None of the `link` elements have any of the `attributes` we specified for the `assets` or `links`. 464 | 465 | This is because `HtmlWebpackPlugin.options.inject` needs to be set to `true` for `attributes` injection to work. 466 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tags Plugin for the HTML Webpack Plugin 2 | ======================================== 3 | [![npm version](https://badge.fury.io/js/html-webpack-tags-plugin.svg)](https://badge.fury.io/js/html-webpack-tags-plugin) [![Build Status](https://travis-ci.org/jharris4/html-webpack-tags-plugin.svg?branch=master)](https://travis-ci.org/jharris4/html-webpack-tags-plugin) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/Flet/semistandard) 4 | 5 | Enhances [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin) 6 | by letting you specify script or link tags to inject. 7 | 8 | Prior Version 9 | ------------ 10 | 11 | - `html-webpack-tags-plugin` requires **Node >= 10** and `webpack` & `html-webpack-plugin` versions **>= 5**. 12 | - For older versions of webpack and node, please use **version 2.x** of this plugin. 13 | - This plugin used to be called `html-webpack-include-assets-plugin`. 14 | - For versions of Node older than `8.6`, please install [html-webpack-include-assets-plugin version 1.x](https://github.com/jharris4/html-webpack-tags-plugin/releases/tag/1.0.10). 15 | 16 | Motivation 17 | ------------ 18 | 19 | When using a plugin such as [copy-webpack-plugin](https://github.com/webpack-contrib/copy-webpack-plugin) you may have assets output to your build directory that are not detected/output by the html-webpack-plugin. 20 | 21 | This plugin lets you manually resolve such issues, and also lets you inject the webpack `publicPath` or compilation `hash` into your tag paths if you so choose. 22 | 23 | Installation 24 | ------------ 25 | You must be running webpack on node 8.x or higher 26 | 27 | Install the plugin with npm: 28 | ```shell 29 | $ npm install --save-dev html-webpack-tags-plugin 30 | ``` 31 | 32 | Deployment 33 | ---------- 34 | 35 | [html-webpack-deploy-plugin](https://github.com/jharris4/html-webpack-deploy-plugin) is a plugin that enhances this plugin with capabilities such as: 36 | - copying your local files and injecting them as html tags with easy to use syntax 37 | - copying package files from your local node_modules and having them versioned automatically as they are injected as html tags 38 | - easy configuration to stop webpack from processing certain node_modules packages so you can handle shipping certain package bundles yourself 39 | - easy to use CDN settings so you can inject package tags that serve from a CDN for optimal performance 40 | 41 | Basic Usage 42 | ----------- 43 | Require the plugin in your webpack config: 44 | 45 | ```javascript 46 | var HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin'); 47 | ``` 48 | 49 | Add the plugin to your webpack config: 50 | 51 | ```javascript 52 | output: { 53 | publicPath: '/abc/' 54 | }, 55 | plugins: [ 56 | new HtmlWebpackPlugin(), 57 | new HtmlWebpackTagsPlugin({ tags: ['a.js', 'b.css'], append: true }) 58 | ] 59 | ``` 60 | 61 | Which will generate html like this: 62 | 63 | ```html 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ``` 73 | 74 | Configuration 75 | ------- 76 | 77 | ### Default Options 78 | 79 | This plugin will run and do nothing if no options are provided. 80 | 81 | The default options for this plugin are shown below: 82 | 83 | ```js 84 | const url = require('url'); 85 | 86 | const DEFAULT_OPTIONS = { 87 | append: true, 88 | prependExternals: true, 89 | jsExtensions: ['.js'], 90 | cssExtensions: ['.css'], 91 | useHash: false, 92 | addHash: (assetPath, hash) => assetPath + '?' + hash, 93 | hash: undefined, 94 | usePublicPath: true, 95 | addPublicPath: (assetPath, publicPath) => url.resolve(publicPath, assetPath), 96 | publicPath: undefined, 97 | tags: [], 98 | links: [], 99 | scripts: [], 100 | metas: undefined 101 | }; 102 | ``` 103 | 104 | --- 105 | ### Options 106 | 107 | All options for this plugin are validated as soon as the plugin is instantiated. 108 | 109 | The available options are: 110 | 111 | |Name|Type|Default|Description| 112 | |:--:|:--:|:-----:|:----------| 113 | |**`append`**|`{Boolean}`|`true`|Whether to prepend or append the injected tags relative to any existing or webpack bundle tags (should be set to **false** when using any `script` tag **`external`**) | 114 | |**`prependExternals`**|`{Boolean}`|`true`|Whether to default **`append`** to **false** for any ` 143 | 144 | ``` 145 | 146 | And this sample `webpack` config: 147 | ```js 148 | { 149 | entry: { 150 | 'app': 'app.js', 151 | 'style': 'style.css' // also generates style.js 152 | }, 153 | plugins: [ 154 | new HtmlWebpackTagsPlugin({ 155 | append: false, links: 'plugin-a-link', scripts: 'plugin-a-script' 156 | }), 157 | new HtmlWebpackTagsPlugin({ 158 | append: false, links: 'plugin-b-link', scripts: 'plugin-b-script' 159 | }), 160 | new HtmlWebpackTagsPlugin({ 161 | append: true, links: 'plugin-c-link', scripts: 'plugin-c-script' 162 | }), 163 | new HtmlWebpackTagsPlugin({ 164 | append: true, links: 'plugin-d-link', scripts: 'plugin-d-script' 165 | }) 166 | ] 167 | } 168 | ``` 169 | 170 | Will generate approximately this html: 171 | ```html 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | ``` 190 | 191 | --- 192 | 193 | The **`hash`** option is a shortcut that overrides the **`useHash`** and **`addHash`** options: 194 | 195 | ```js 196 | const shortcutFunction = { 197 | hash: (path, hash) => path + '?' + hash 198 | } 199 | const isTheSameAsFunction = { 200 | useHash: true, 201 | addHash: (path, hash) => path + '?' + hash 202 | } 203 | 204 | const shortcutDisabled = { 205 | hash: false 206 | } 207 | const isTheSameAsDisabled = { 208 | useHash: false, 209 | } 210 | ``` 211 | 212 | --- 213 | 214 | The **`publicPath`** option is a shortcut that overrides the **`usePublicPath`** and **`addPublicPath`** options: 215 | 216 | ```js 217 | const shortcutFunction = { 218 | publicPath: (path, publicPath) => publicPath + path 219 | } 220 | const isTheSameAsFunction = { 221 | usePublicPath: true, 222 | addPublicPath: (path, publicPath) => publicPath + path 223 | } 224 | 225 | const shortcutDisabled = { 226 | publicPath: false 227 | } 228 | const isTheSameAsDisabled = { 229 | usePublicPath: false, 230 | } 231 | 232 | const shortcutString = { 233 | publicPath: 'myValue' 234 | } 235 | const isTheSameAsString = { 236 | usePublicPath: true, 237 | addPublicPath: (path) => 'myValue' + path 238 | } 239 | 240 | ``` 241 | 242 | --- 243 | 244 | When the **`tags`** option is used the type of the specified tag(s) is inferred either from the file extension or an optional **`type`** option that may be one of: `'js' \| 'css'`| 245 | 246 | The inferred type is used to split the **`tags`** option into `tagLinks` and `tagScripts` that are injected **before** any specified **`links`** or **`scripts`** options. 247 | 248 | The following are functionally equivalent: 249 | 250 | ```js 251 | new HtmlWebpackTagsPlugin({ 252 | tags: [ 253 | 'style-1.css', 254 | { path: 'script-2.js' }, 255 | { path: 'script-3-not-js.css', type: 'js' }, 256 | 'style-4.css' 257 | ] 258 | }); 259 | 260 | new HtmlWebpackTagsPlugin({ 261 | links: [ 262 | 'style-1.css', 263 | 'style-4.css' 264 | ], 265 | scripts: [ 266 | { path: 'script-2.js' }, 267 | { path: 'script-3-not-js.css' } 268 | ] 269 | }); 270 | ``` 271 | --- 272 | 273 | The `value` of the **`tags`**, **`links`** or **`scripts`** options can be specified in several ways: 274 | 275 | - as a **String**: 276 | 277 | ```js 278 | new HtmlWebpackTagsPlugin({ tags: 'style.css' }); 279 | ``` 280 | 281 | - as an **Object**: 282 | 283 | ```js 284 | new HtmlWebpackTagsPlugin({ links: { path: 'style.css' } }); 285 | ``` 286 | 287 | - as an **Array** of **String**s or **Object**s: 288 | 289 | ```js 290 | new HtmlWebpackTagsPlugin({ 291 | scripts: [ 292 | 'aScript.js', 293 | { 294 | path: 'bScript.js' 295 | }, 296 | 'cScript.js' 297 | ] 298 | }); 299 | ``` 300 | 301 | --- 302 | 303 | When tags are specified as **Object**s, the following `tag object` options are available: 304 | 305 | |Name|Type|Default|Description| 306 | |:--:|:--:|:-----:|:----------| 307 | |**`path`**|`{String}`|**`required*`**|The tag file path (used for `` or ` 715 | 716 | ``` 717 | 718 | 719 | _____ 720 | 721 | Specifying **`scripts`** with **`external`** options: 722 | 723 | ```javascript 724 | output: { 725 | publicPath: '/my-public-path/' 726 | }, 727 | plugins: [ 728 | new CopyWebpackPlugin([ 729 | { from: 'node_modules/bootstrap/dist/js', to: 'js/'} 730 | ]), 731 | new HtmlWebpackPlugin(), 732 | new HtmlWebpackTagsPlugin({ 733 | tags: [], 734 | scripts: [ 735 | { 736 | path: 'asset/path', 737 | external: { 738 | packageName: 'react', 739 | variableName: 'React' 740 | }, 741 | attributes: { 742 | type: 'text/javascript' 743 | } 744 | } 745 | ] 746 | }) 747 | ] 748 | ``` 749 | 750 | Will add the following `properties` to the `webpack.compilation.options.externals`: 751 | 752 | ```js 753 | const compilationConfig = { 754 | ...otherProperties, 755 | externals: { 756 | "react": "React" 757 | } 758 | }; 759 | ``` 760 | 761 | This can be useful to control which packages webpack is bundling versus ones you can serve from a CDN. 762 | 763 | Note that `script` tags with **`external`** specified need to be placed **before** the webpack bundle tags. 764 | 765 | This means that you should always set **`append`** to **false** when using the `script` **`external`** option. 766 | 767 | The **`prependExternals`** option was added in `2.0.10` to handle this case automatically. 768 | 769 | 770 | _____ 771 | 772 | Using the **`metas`** option to inject `meta` tags: 773 | 774 | ```javascript 775 | output: { 776 | publicPath: '/my-public-path/' 777 | }, 778 | plugins: [ 779 | new CopyWebpackPlugin([ 780 | { from: 'node_modules/bootstrap/dist/js', to: 'js/'} 781 | ]), 782 | new HtmlWebpackPlugin(), 783 | new HtmlWebpackTagsPlugin({ 784 | metas: [ 785 | { 786 | path: 'asset/path', 787 | attributes: { 788 | name: 'the-meta-name' 789 | } 790 | } 791 | ] 792 | }) 793 | ] 794 | ``` 795 | 796 | Will inject the following `` element into the index template html 797 | 798 | ```html 799 | 800 | 801 | 802 | 803 | ``` 804 | 805 | Note that the **`append`** settings has no effect on how the `` elements are injected. 806 | 807 | 808 | _____ 809 | 810 | Caveats 811 | ------- 812 | 813 | ___ 814 | 815 | #### Plugin Ordering 816 | 817 | Some users have encountered issues with plugin ordering. 818 | 819 | - It is advisable to always place any `HtmlWebpackPlugin` plugins **before** any `HtmlWebpackTagsPlugin` plugins in your webpack config. 820 | 821 | - When **`append`** is **false** tags are injected before any other tags. This means that if you have two instances of this plugin both with append set to false, then the `second` plugin's tags will be injected **before** the `first` plugin's tags. 822 | 823 | --- 824 | 825 | #### Webpack `externals` 826 | 827 | Setting the **`external`** option for a `script` `tag object` requires caution to ensure that the scripts are in the correct order. 828 | 829 | - It is advisable to always set **`append`** to **false** so that `external` \ tags are always inserted **before** the `webpack` bundle \ tags. 830 | 831 | - The order that you use when you specify a list of external links matters. For example, ` 855 | <% } %> 856 | 857 | 858 | ``` 859 | 860 | The default templating engine for `html-webpack-plugin` seems to be based on **`lodash`**. 861 | 862 | With the above template we might use the following `webpack` config which **disables** **`inject`**: 863 | 864 | ```javascript 865 | output: { 866 | publicPath: '/the-public-path/' 867 | }, 868 | plugins: [ 869 | new HtmlWebpackPlugin({ inject: false }), 870 | new HtmlWebpackTagsPlugin({ 871 | tags: [{ path: 'css/bootstrap-theme.min.css', attributes: { id: 'bootstrapTheme' } }], 872 | links: [{ href: 'the-ref', attributes: { rel: 'icon' } }], 873 | append: true 874 | }) 875 | ] 876 | ``` 877 | 878 | The problem is that the `template syntax` does not seem to allow injection of more than `one attribute value`, namely the `path` (**`href`** or **`src`**) 879 | 880 | This means it will **generate** an `index.html` that is **missing** all of the script **`attributes`** like this: 881 | 882 | ```html 883 | 884 | 885 | 886 | 887 | ``` 888 | 889 | If the templating engine supports injection of **entire tags** instead of just the `href`/`src` attribute value then working with **`inject`** set to **false** may be possible. 890 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const webpack = require('webpack'); 5 | const assert = require('assert'); 6 | const minimatch = require('minimatch'); 7 | const glob = require('glob'); 8 | const slash = require('slash'); // fixes slashes in file paths for windows 9 | 10 | const PLUGIN_NAME = 'HtmlWebpackTagsPlugin'; 11 | 12 | const IS = { 13 | isDefined: v => v !== undefined, 14 | isObject: v => v !== null && v !== undefined && typeof v === 'object' && !Array.isArray(v), 15 | isBoolean: v => v === true || v === false, 16 | isNumber: v => v !== undefined && (typeof v === 'number' || v instanceof Number) && isFinite(v), 17 | isString: v => v !== null && v !== undefined && (typeof v === 'string' || v instanceof String), 18 | isArray: v => Array.isArray(v), 19 | isFunction: v => typeof v === 'function' 20 | }; 21 | 22 | const { isDefined, isObject, isBoolean, isNumber, isString, isArray, isFunction } = IS; 23 | 24 | const DEFAULT_OPTIONS = { 25 | append: true, 26 | prependExternals: true, 27 | useHash: false, 28 | addHash: (assetPath, hash) => assetPath + '?' + hash, 29 | usePublicPath: true, 30 | addPublicPath: (assetPath, publicPath) => (publicPath !== '' && !publicPath.endsWith('/') && !assetPath.startsWith('/')) ? publicPath + '/' + assetPath : publicPath + assetPath, 31 | jsExtensions: ['.js'], 32 | cssExtensions: ['.css'], 33 | tags: [], 34 | links: [], 35 | scripts: [] 36 | }; 37 | 38 | const ASSET_TYPE_CSS = 'css'; 39 | const ASSET_TYPE_JS = 'js'; 40 | 41 | const ASSET_TYPES = [ASSET_TYPE_CSS, ASSET_TYPE_JS]; 42 | 43 | const ATTRIBUTES_TEXT = 'strings, booleans or numbers'; 44 | 45 | const isValidAttributeValue = v => isString(v) || isBoolean(v) || isNumber(v); 46 | 47 | const isType = type => ASSET_TYPES.indexOf(type) !== -1; 48 | 49 | const isTypeCss = type => type === ASSET_TYPE_CSS; 50 | 51 | const isFunctionReturningString = v => isFunction(v) && isString(v('', '')); 52 | 53 | const isArrayOfString = v => isArray(v) && v.every(i => isString(i)); 54 | 55 | const createExtensionsRegex = extensions => new RegExp(`.*(${extensions.join('|')})$`); 56 | 57 | const getExtensions = (options, optionExtensionName, optionPath) => { 58 | let extensions = DEFAULT_OPTIONS[optionExtensionName]; 59 | if (isDefined(options[optionExtensionName])) { 60 | if (isString(options[optionExtensionName])) { 61 | extensions = [options[optionExtensionName]]; 62 | } else { 63 | extensions = options[optionExtensionName]; 64 | assert(isArray(extensions), `${optionPath}.${optionExtensionName} should be a string or array of strings (${extensions})`); 65 | extensions.forEach(function (extension) { 66 | assert(isString(extension), `${optionPath}.${optionExtensionName} array should only contain strings (${extension})`); 67 | }); 68 | } 69 | } 70 | return extensions; 71 | }; 72 | 73 | const getHasExtensions = (options, optionExtensionName, optionPath) => { 74 | const regexp = createExtensionsRegex(getExtensions(options, optionExtensionName, optionPath)); 75 | return value => regexp.test(value); 76 | }; 77 | 78 | const getAssetTypeCheckers = (options, optionPath) => { 79 | const hasJsExtensions = getHasExtensions(options, 'jsExtensions', optionPath); 80 | const hasCssExtensions = getHasExtensions(options, 'cssExtensions', optionPath); 81 | return { 82 | isAssetTypeCss (value) { 83 | return hasCssExtensions(value); 84 | }, 85 | isAssetTypeJs (value) { 86 | return hasJsExtensions(value); 87 | } 88 | }; 89 | }; 90 | 91 | const splitLinkScriptTags = (tagObjects, options, optionName, optionPath) => { 92 | const linkObjects = []; 93 | const scriptObjects = []; 94 | const { isAssetTypeCss, isAssetTypeJs } = getAssetTypeCheckers(options, optionPath); 95 | 96 | tagObjects.forEach(tagObject => { 97 | if (isDefined(tagObject.type)) { 98 | const { type, ...others } = tagObject; 99 | assert(isType(type), `${optionPath}.${optionName} type must be css or js (${type})`); 100 | (isTypeCss(type) ? linkObjects : scriptObjects).push({ 101 | ...others 102 | }); 103 | } else { 104 | const { path } = tagObject; 105 | if (isAssetTypeCss(path)) { 106 | linkObjects.push(tagObject); 107 | } else if (isAssetTypeJs(path)) { 108 | scriptObjects.push(tagObject); 109 | } else { 110 | assert(false, `${optionPath}.${optionName} could not determine asset type for (${path})`); 111 | } 112 | } 113 | }); 114 | 115 | return [linkObjects, scriptObjects]; 116 | }; 117 | 118 | const getTagObjects = (tag, optionName, optionPath, isMetaTag = false) => { 119 | let tagObjects; 120 | if (isMetaTag) { 121 | assert(isObject(tag), `${optionPath}.${optionName} items must be an object`); 122 | } else { 123 | assert(isString(tag) || isObject(tag), `${optionPath}.${optionName} items must be an object or string`); 124 | } 125 | if (!isMetaTag && isString(tag)) { 126 | tagObjects = [{ 127 | path: tag 128 | }]; 129 | } else { 130 | if (isMetaTag) { 131 | if (isDefined(tag.path)) { 132 | assert(isString(tag.path), `${optionPath}.${optionName} object should have a string path property`); 133 | } 134 | } else { 135 | assert(isString(tag.path), `${optionPath}.${optionName} object must have a string path property`); 136 | } 137 | if (isDefined(tag.sourcePath)) { 138 | assert(isString(tag.sourcePath), `${optionPath}.${optionName} object should have a string sourcePath property`); 139 | } 140 | if (isMetaTag) { 141 | assert(isDefined(tag.attributes), `${optionPath}.${optionName} object must have an object attributes property`); 142 | assert(Object.keys(tag.attributes).length > 0, `${optionPath}.${optionName} object must have a non empty object attributes property`); 143 | } 144 | if (isDefined(tag.attributes)) { 145 | const { attributes } = tag; 146 | assert(isObject(attributes), `${optionPath}.${optionName} object should have an object attributes property`); 147 | Object.keys(attributes).forEach(attribute => { 148 | const value = attributes[attribute]; 149 | assert(isValidAttributeValue(value), `${optionPath}.${optionName} object attribute values should be ` + ATTRIBUTES_TEXT); 150 | }); 151 | } 152 | 153 | tag = getValidatedMainOptions(tag, `${optionPath}.${optionName}`, {}); 154 | 155 | if (isDefined(tag.glob) || isDefined(tag.globPath) || isDefined(tag.globFlatten)) { 156 | if (isMetaTag) { 157 | assert(isDefined(tag.path), `${optionPath}.${optionName} object must have a path property when glob is used`); 158 | } 159 | const { glob: assetGlob, globPath, globFlatten, ...otherAssetProperties } = tag; 160 | assert(isString(assetGlob), `${optionPath}.${optionName} object should have a string glob property`); 161 | assert(isString(globPath), `${optionPath}.${optionName} object should have a string globPath property`); 162 | if (isDefined(globFlatten)) { 163 | assert(isBoolean(globFlatten), `${optionPath}.${optionName} object should have a boolean globFlatten property`); 164 | } 165 | const flatten = isDefined(globFlatten) ? globFlatten : false; 166 | const globAssets = glob.sync(assetGlob, { cwd: globPath }); 167 | const globAssetPaths = globAssets.map(globAsset => slash(path.join(tag.path, flatten ? path.basename(globAsset) : globAsset))); 168 | assert(globAssetPaths.length > 0, `${optionPath}.${optionName} object glob found no files (${tag.path} ${assetGlob} ${globPath})`); 169 | tagObjects = []; 170 | globAssetPaths.forEach(globAssetPath => { 171 | tagObjects.push({ 172 | ...otherAssetProperties, 173 | path: globAssetPath 174 | }); 175 | }); 176 | } else { 177 | tagObjects = [tag]; 178 | } 179 | } 180 | return tagObjects; 181 | }; 182 | 183 | const getValidatedTagObjects = (options, optionName, optionPath) => { 184 | let tagObjects; 185 | if (isDefined(options[optionName])) { 186 | const tags = options[optionName]; 187 | assert(isString(tags) || isObject(tags) || isArray(tags), `${optionPath}.${optionName} should be a string, object, or array (${tags})`); 188 | if (isArray(tags)) { 189 | tagObjects = []; 190 | tags.forEach(asset => { 191 | tagObjects = tagObjects.concat(getTagObjects(asset, optionName, optionPath)); 192 | }); 193 | } else { 194 | tagObjects = getTagObjects(tags, optionName, optionPath); 195 | } 196 | } 197 | return tagObjects; 198 | }; 199 | 200 | const getValidatedMetaObjects = (options, optionName, optionPath) => { 201 | let metaObjects; 202 | if (isDefined(options[optionName])) { 203 | const tags = options[optionName]; 204 | assert(isObject(tags) || isArray(tags), `${optionPath}.${optionName} should be an object or array (${tags})`); 205 | if (isArray(tags)) { 206 | metaObjects = []; 207 | tags.forEach(asset => { 208 | metaObjects = metaObjects.concat(getTagObjects(asset, optionName, optionPath, true)); 209 | }); 210 | } else { 211 | metaObjects = getTagObjects(tags, optionName, optionPath, true); 212 | } 213 | } 214 | return metaObjects; 215 | }; 216 | 217 | const getValidatedTagObjectExternals = (tagObjects, isScript, optionName, optionPath) => { 218 | return tagObjects.map(tagObject => { 219 | if (isObject(tagObject) && isDefined(tagObject.external)) { 220 | const { external } = tagObject; 221 | if (isScript) { 222 | assert(isObject(external), `${optionPath}.${optionName}.external should be an object`); 223 | const { packageName, variableName } = external; 224 | assert(isString(packageName) || isString(variableName), `${optionPath}.${optionName}.external should have a string packageName and variableName property`); 225 | assert(isString(packageName), `${optionPath}.${optionName}.external should have a string packageName property`); 226 | assert(isString(variableName), `${optionPath}.${optionName}.external should have a string variableName property`); 227 | } else { 228 | assert(false, `${optionPath}.${optionName}.external should not be used on non script tags`); 229 | } 230 | } 231 | return tagObject; 232 | }); 233 | }; 234 | 235 | const getShouldSkip = files => { 236 | let shouldSkip = () => false; 237 | if (isDefined(files)) { 238 | shouldSkip = htmlPluginData => !files.some(function (file) { 239 | return minimatch(htmlPluginData.outputName, file); 240 | }); 241 | } 242 | return shouldSkip; 243 | }; 244 | 245 | const processShortcuts = (options, optionPath, keyShortcut, keyUse, keyAdd, add) => { 246 | const processedOptions = {}; 247 | if (isDefined(options[keyUse]) || isDefined(options[keyAdd])) { 248 | assert(!isDefined(options[keyShortcut]), `${optionPath}.${keyShortcut} should not be used with either ${keyUse} or ${keyAdd}`); 249 | if (isDefined(options[keyUse])) { 250 | assert(isBoolean(options[keyUse]), `${optionPath}.${keyUse} should be a boolean`); 251 | processedOptions[keyUse] = options[keyUse]; 252 | } 253 | if (isDefined(options[keyAdd])) { 254 | assert(isFunctionReturningString(options[keyAdd]), `${optionPath}.${keyAdd} should be a function that returns a string`); 255 | processedOptions[keyAdd] = options[keyAdd]; 256 | } 257 | } else if (isDefined(options[keyShortcut])) { 258 | const shortcut = options[keyShortcut]; 259 | assert(isBoolean(shortcut) || isString(shortcut) || isFunctionReturningString(shortcut), 260 | `${optionPath}.${keyShortcut} should be a boolean or a string or a function that returns a string`); 261 | if (isBoolean(shortcut)) { 262 | processedOptions[keyUse] = shortcut; 263 | } else if (isString(shortcut)) { 264 | processedOptions[keyUse] = true; 265 | processedOptions[keyAdd] = path => add(path, shortcut); 266 | } else { 267 | processedOptions[keyUse] = true; 268 | processedOptions[keyAdd] = shortcut; 269 | } 270 | } 271 | return processedOptions; 272 | }; 273 | 274 | const getValidatedMainOptions = (options, optionPath, defaultOptions = {}) => { 275 | const { append, prependExternals, publicPath, usePublicPath, addPublicPath, hash, useHash, addHash, ...otherOptions } = options; 276 | const validatedOptions = { ...defaultOptions, ...otherOptions }; 277 | if (isDefined(append)) { 278 | assert(isBoolean(append), `${optionPath}.append should be a boolean`); 279 | validatedOptions.append = append; 280 | } 281 | if (isDefined(prependExternals)) { 282 | assert(isBoolean(prependExternals), `${optionPath}.prependExternals should be a boolean`); 283 | validatedOptions.prependExternals = prependExternals; 284 | } 285 | const publicPathOptions = processShortcuts(options, optionPath, 'publicPath', 'usePublicPath', 'addPublicPath', DEFAULT_OPTIONS.addPublicPath); 286 | if (isDefined(publicPathOptions.usePublicPath)) { 287 | validatedOptions.usePublicPath = publicPathOptions.usePublicPath; 288 | } 289 | if (isDefined(publicPathOptions.addPublicPath)) { 290 | validatedOptions.addPublicPath = publicPathOptions.addPublicPath; 291 | } 292 | const hashOptions = processShortcuts(options, optionPath, 'hash', 'useHash', 'addHash', DEFAULT_OPTIONS.addHash); 293 | if (isDefined(hashOptions.useHash)) { 294 | validatedOptions.useHash = hashOptions.useHash; 295 | } 296 | if (isDefined(hashOptions.addHash)) { 297 | validatedOptions.addHash = hashOptions.addHash; 298 | } 299 | return validatedOptions; 300 | }; 301 | 302 | const getValidatedOptions = (options, optionPath, defaultOptions = DEFAULT_OPTIONS) => { 303 | assert(isObject(options), `${optionPath} should be an object`); 304 | let validatedOptions = { ...defaultOptions }; 305 | validatedOptions = { 306 | ...validatedOptions, 307 | ...getValidatedMainOptions(options, optionPath, defaultOptions) 308 | }; 309 | const { append: globalAppend, prependExternals } = validatedOptions; 310 | 311 | const getAppend = prependExternals ? external => (isDefined(external) ? false : globalAppend) : () => globalAppend; 312 | 313 | const isTagPrepend = ({ append, external }) => isDefined(append) ? !append : !getAppend(external); 314 | const isTagAppend = ({ append, external }) => isDefined(append) ? append : getAppend(external); 315 | 316 | const hasTags = isDefined(options.tags); 317 | if (hasTags) { 318 | const tagObjects = getValidatedTagObjects(options, 'tags', optionPath); 319 | let [linkObjects, scriptObjects] = splitLinkScriptTags(tagObjects, options, 'tags', optionPath); 320 | linkObjects = getValidatedTagObjectExternals(linkObjects, false, 'tags', optionPath); 321 | scriptObjects = getValidatedTagObjectExternals(scriptObjects, true, 'tags', optionPath); 322 | validatedOptions.links = linkObjects; 323 | validatedOptions.scripts = scriptObjects; 324 | } 325 | if (isDefined(options.links)) { 326 | let linkObjects = getValidatedTagObjects(options, 'links', optionPath); 327 | linkObjects = getValidatedTagObjectExternals(linkObjects, false, 'links', optionPath); 328 | validatedOptions.links = hasTags ? validatedOptions.links.concat(linkObjects) : linkObjects; 329 | } 330 | if (isDefined(options.scripts)) { 331 | let scriptObjects = getValidatedTagObjects(options, 'scripts', optionPath); 332 | scriptObjects = getValidatedTagObjectExternals(scriptObjects, true, 'scripts', optionPath); 333 | validatedOptions.scripts = hasTags ? validatedOptions.scripts.concat(scriptObjects) : scriptObjects; 334 | } 335 | if (isDefined(validatedOptions.links)) { 336 | validatedOptions.linksPrepend = validatedOptions.links.filter(isTagPrepend); 337 | validatedOptions.linksAppend = validatedOptions.links.filter(isTagAppend); 338 | } 339 | if (isDefined(validatedOptions.scripts)) { 340 | validatedOptions.scriptsPrepend = validatedOptions.scripts.filter(isTagPrepend); 341 | validatedOptions.scriptsAppend = validatedOptions.scripts.filter(isTagAppend); 342 | } 343 | if (isDefined(options.metas)) { 344 | let metaObjects = getValidatedMetaObjects(options, 'metas', optionPath); 345 | metaObjects = getValidatedTagObjectExternals(metaObjects, false, 'metas', optionPath); 346 | validatedOptions.metas = metaObjects; 347 | } 348 | 349 | return validatedOptions; 350 | }; 351 | 352 | const getTagPath = (tagObject, options, webpackPublicPath, compilationHash) => { 353 | const mergedOptions = { ...options }; 354 | Object.keys(tagObject).filter(key => isDefined(tagObject[key])).forEach(key => { 355 | mergedOptions[key] = tagObject[key]; 356 | }); 357 | const { usePublicPath, addPublicPath, useHash, addHash } = mergedOptions; 358 | 359 | let { path } = tagObject; 360 | if (usePublicPath) { 361 | path = addPublicPath(path, webpackPublicPath); 362 | } 363 | if (useHash) { 364 | path = addHash(path, compilationHash); 365 | } 366 | return slash(path); 367 | }; 368 | 369 | const getAllValidatedOptions = (options, optionPath) => { 370 | const validatedOptions = getValidatedOptions(options, optionPath); 371 | let { files } = options; 372 | if (isDefined(files)) { 373 | assert((isString(files) || isArrayOfString(files)), `${optionPath}.files should be a string or array of strings`); 374 | if (isString(files)) { 375 | files = [files]; 376 | } 377 | return { 378 | ...validatedOptions, 379 | files 380 | }; 381 | } 382 | return validatedOptions; 383 | }; 384 | 385 | function HtmlWebpackTagsPlugin (options) { 386 | const validatedOptions = getAllValidatedOptions(options, PLUGIN_NAME + '.options'); 387 | 388 | const shouldSkip = getShouldSkip(validatedOptions.files); 389 | 390 | // Allows tests to be run with html-webpack-plugin v4 391 | const htmlPluginName = isDefined(options.htmlPluginName) ? options.htmlPluginName : 'html-webpack-plugin'; 392 | 393 | this.options = { 394 | ...validatedOptions, 395 | shouldSkip, 396 | htmlPluginName 397 | }; 398 | } 399 | 400 | HtmlWebpackTagsPlugin.prototype.apply = function (compiler) { 401 | const { options } = this; 402 | const { shouldSkip, htmlPluginName } = options; 403 | const { scripts, scriptsPrepend, scriptsAppend, linksPrepend, linksAppend, metas } = options; 404 | 405 | const externals = compiler.options.externals || {}; 406 | scripts.forEach(script => { 407 | const { external } = script; 408 | if (isObject(external)) { 409 | externals[external.packageName] = external.variableName; 410 | } 411 | }); 412 | compiler.options.externals = externals; 413 | 414 | let savedAssetsPublicPath = null; 415 | 416 | // Hook into the html-webpack-plugin processing 417 | const onCompilation = compilation => { 418 | const onBeforeHtmlGeneration = (htmlPluginData, callback) => { 419 | if (shouldSkip(htmlPluginData)) { 420 | if (callback) { 421 | return callback(null, htmlPluginData); 422 | } else { 423 | return Promise.resolve(htmlPluginData); 424 | } 425 | } 426 | 427 | const { assets } = htmlPluginData; 428 | const pluginPublicPath = savedAssetsPublicPath = assets.publicPath; 429 | const compilationHash = compilation.hash; 430 | const assetPromises = []; 431 | 432 | const addAsset = assetPath => { 433 | try { 434 | if (htmlPluginData.plugin && htmlPluginData.plugin.addFileToAssets) { 435 | return htmlPluginData.plugin.addFileToAssets(assetPath, compilation); 436 | } else { 437 | assetPath = path.resolve(compilation.compiler.context, assetPath); 438 | return Promise.all([ 439 | new Promise((resolve, reject) => { 440 | fs.stat(assetPath, (err, stats) => { 441 | if (err) { 442 | reject(err); 443 | } else { 444 | resolve(stats); 445 | } 446 | }); 447 | }), 448 | new Promise((resolve, reject) => { 449 | fs.readFile(assetPath, (err, data) => { 450 | if (err) { 451 | reject(err); 452 | } else { 453 | resolve(data); 454 | } 455 | }); 456 | }) 457 | ]).then(([stat, source]) => { 458 | const { size } = stat; 459 | const basename = path.basename(assetPath); 460 | source = new webpack.sources.RawSource(source, true); 461 | compilation.fileDependencies.add(assetPath); 462 | compilation.emitAsset(basename, source, { size }); 463 | }); 464 | } 465 | } catch (err) { 466 | return Promise.reject(err); 467 | } 468 | }; 469 | 470 | const getPath = tag => { 471 | if (isString(tag.sourcePath)) { 472 | assetPromises.push(addAsset(tag.sourcePath)); 473 | } 474 | return getTagPath(tag, options, pluginPublicPath, compilationHash); 475 | }; 476 | 477 | const jsPrependPaths = scriptsPrepend.map(getPath); 478 | const jsAppendPaths = scriptsAppend.map(getPath); 479 | 480 | const cssPrependPaths = linksPrepend.map(getPath); 481 | const cssAppendPaths = linksAppend.map(getPath); 482 | 483 | assets.js = jsPrependPaths.concat(assets.js).concat(jsAppendPaths); 484 | assets.css = cssPrependPaths.concat(assets.css).concat(cssAppendPaths); 485 | 486 | if (metas) { 487 | metas.forEach(tag => { 488 | if (isString(tag.sourcePath)) { 489 | assetPromises.push(addAsset(tag.sourcePath)); 490 | } 491 | }); 492 | } 493 | 494 | Promise.all(assetPromises).then( 495 | () => { 496 | if (callback) { 497 | callback(null, htmlPluginData); 498 | } else { 499 | return Promise.resolve(htmlPluginData); 500 | } 501 | }, 502 | (err) => { 503 | if (callback) { 504 | callback(err); 505 | } else { 506 | return Promise.reject(err); 507 | } 508 | } 509 | ); 510 | }; 511 | 512 | const onAlterAssetTagGroups = (htmlPluginData, callback) => { 513 | if (shouldSkip(htmlPluginData)) { 514 | if (callback) { 515 | return callback(null, htmlPluginData); 516 | } else { 517 | return Promise.resolve(htmlPluginData); 518 | } 519 | } 520 | 521 | const pluginHead = htmlPluginData.head ? htmlPluginData.head : htmlPluginData.headTags; 522 | const pluginBody = htmlPluginData.body ? htmlPluginData.body : htmlPluginData.bodyTags; 523 | 524 | if (metas) { 525 | const pluginPublicPath = savedAssetsPublicPath; 526 | const compilationHash = compilation.hash; 527 | 528 | const getMeta = tag => { 529 | if (isDefined(tag.path)) { 530 | return { 531 | tagName: 'meta', 532 | attributes: { 533 | content: getTagPath(tag, options, pluginPublicPath, compilationHash), 534 | ...tag.attributes 535 | } 536 | }; 537 | } else { 538 | return { 539 | tagName: 'meta', 540 | attributes: tag.attributes 541 | }; 542 | } 543 | }; 544 | pluginHead.push(...metas.map(getMeta)); 545 | } 546 | 547 | const injectOption = htmlPluginData.plugin.options.inject; 548 | const sourceScripts = injectOption === 'body' ? pluginBody : pluginHead; 549 | 550 | const pluginLinks = pluginHead.filter(({ tagName }) => tagName === 'link'); 551 | const pluginScripts = sourceScripts.filter(({ tagName }) => tagName === 'script'); 552 | 553 | const headPrepend = pluginLinks.slice(0, linksPrepend.length); 554 | const headAppend = pluginLinks.slice(pluginLinks.length - linksAppend.length); 555 | 556 | const bodyPrepend = pluginScripts.slice(0, scriptsPrepend.length); 557 | const bodyAppend = pluginScripts.slice(pluginScripts.length - scriptsAppend.length); 558 | 559 | const copyAttributes = (tags, tagObjects) => { 560 | tags.forEach((tag, i) => { 561 | const { attributes } = tagObjects[i]; 562 | if (attributes) { 563 | const { attributes: tagAttributes } = tag; 564 | Object.keys(attributes).forEach(attribute => { 565 | tagAttributes[attribute] = attributes[attribute]; 566 | }); 567 | } 568 | }); 569 | }; 570 | 571 | copyAttributes(headPrepend.concat(headAppend), linksPrepend.concat(linksAppend)); 572 | copyAttributes(bodyPrepend.concat(bodyAppend), scriptsPrepend.concat(scriptsAppend)); 573 | 574 | if (callback) { 575 | callback(null, htmlPluginData); 576 | } else { 577 | return Promise.resolve(htmlPluginData); 578 | } 579 | }; 580 | 581 | const HtmlWebpackPlugin = require(htmlPluginName); 582 | if (HtmlWebpackPlugin.getHooks) { 583 | const hooks = HtmlWebpackPlugin.getHooks(compilation); 584 | const htmlPlugins = compilation.options.plugins.filter(plugin => plugin instanceof HtmlWebpackPlugin); 585 | if (htmlPlugins.length === 0) { 586 | const message = "Error running html-webpack-tags-plugin, are you sure you have html-webpack-plugin before it in your webpack config's plugins?"; 587 | throw new Error(message); 588 | } 589 | hooks.beforeAssetTagGeneration.tapAsync('htmlWebpackTagsPlugin', onBeforeHtmlGeneration); 590 | hooks.alterAssetTagGroups.tapAsync('htmlWebpackTagsPlugin', onAlterAssetTagGroups); 591 | } else { 592 | const message = "Error running html-webpack-tags-plugin, are you sure you have html-webpack-plugin before it in your webpack config's plugins?"; 593 | throw new Error(message); 594 | } 595 | }; 596 | 597 | compiler.hooks.compilation.tap('htmlWebpackTagsPlugin', onCompilation); 598 | }; 599 | 600 | HtmlWebpackTagsPlugin.api = { 601 | IS, 602 | getValidatedOptions 603 | }; 604 | 605 | module.exports = HtmlWebpackTagsPlugin; 606 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-webpack-tags-plugin", 3 | "version": "3.0.2", 4 | "description": "lets you define html tags to inject with html-webpack-plugin", 5 | "main": "index.js", 6 | "types": "typings.d.ts", 7 | "engines": { 8 | "node": ">=10" 9 | }, 10 | "files": [ 11 | "index.js", 12 | "typings.d.ts" 13 | ], 14 | "scripts": { 15 | "prepublish": "npm run test", 16 | "pretest": "semistandard", 17 | "test": "jasmine", 18 | "testfilter": "jasmine --filter=\"name of suite or test\"", 19 | "debug": "node-debug jasmine" 20 | }, 21 | "semistandard": { 22 | "ignore": [ 23 | "spec/**/*-bundle.js", 24 | "spec/dist/**" 25 | ] 26 | }, 27 | "unmockedModulePathPatterns": [ 28 | "jasmine-expect" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/jharris4/html-webpack-tags-plugin.git" 33 | }, 34 | "keywords": [ 35 | "webpack", 36 | "plugin", 37 | "html-webpack-plugin", 38 | "html", 39 | "tags", 40 | "inject", 41 | "include", 42 | "assets" 43 | ], 44 | "author": "Jon Harris (https://github.com/jharris4)", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/jharris4/html-webpack-tags-plugin/issues" 48 | }, 49 | "homepage": "https://github.com/jharris4/html-webpack-tags-plugin", 50 | "peerDependencies": { 51 | "html-webpack-plugin": "^5.0.0", 52 | "webpack": "^5.0.0" 53 | }, 54 | "devDependencies": { 55 | "cheerio": "1.0.0-rc.10", 56 | "copy-webpack-plugin": "^9.0.1", 57 | "css-loader": "^6.2.0", 58 | "express": "^4.17.1", 59 | "html-webpack-plugin": "^5.5.0", 60 | "jasmine": "^3.10.0", 61 | "jasmine-expect": "^5.0.0", 62 | "mini-css-extract-plugin": "^2.4.3", 63 | "puppeteer": "^10.4.0", 64 | "rimraf": "^3.0.2", 65 | "semistandard": "^16.0.1", 66 | "style-loader": "^3.3.1", 67 | "webpack": "^5.61.0" 68 | }, 69 | "dependencies": { 70 | "glob": "^7.2.0", 71 | "minimatch": "^3.0.4", 72 | "slash": "^3.0.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /spec/api.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | require('jasmine-expect'); 3 | 4 | const HtmlWebpackTagsPlugin = require('../'); 5 | 6 | describe('api', () => { 7 | it('exports the api', done => { 8 | const { api } = HtmlWebpackTagsPlugin; 9 | expect(api && typeof api === 'object' && !Array.isArray(api)); 10 | done(); 11 | }); 12 | 13 | it('exports the api IS', done => { 14 | const { IS } = HtmlWebpackTagsPlugin.api; 15 | expect(typeof IS === 'object' && typeof IS.isDefined === 'function'); 16 | done(); 17 | }); 18 | 19 | it('exports the api getValidatedOptions', done => { 20 | const { getValidatedMainOptions } = HtmlWebpackTagsPlugin.api; 21 | expect(typeof getValidatedMainOptions === 'function'); 22 | done(); 23 | }); 24 | 25 | describe('getValidatedOptions', () => { 26 | it('should throw with the right error for bad options', done => { 27 | const { getValidatedOptions } = HtmlWebpackTagsPlugin.api; 28 | expect(typeof getValidatedOptions === 'function'); 29 | const theFunction = () => { 30 | return getValidatedOptions({ append: '123' }, 'pluginName.options'); 31 | }; 32 | 33 | expect(theFunction).toThrowError(/(pluginName.options.append should be a boolean)/); 34 | done(); 35 | }); 36 | 37 | it('should return the right options for valid options', done => { 38 | const { getValidatedOptions } = HtmlWebpackTagsPlugin.api; 39 | expect(typeof getValidatedOptions === 'function'); 40 | const theFunction = () => { 41 | return getValidatedOptions({ append: true }, 'pluginName.options', {}); 42 | }; 43 | 44 | expect(theFunction).not.toThrowError(); 45 | expect(theFunction()).toEqual({ append: true }); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should throw with the right error for bad links options', done => { 51 | const { getValidatedOptions } = HtmlWebpackTagsPlugin.api; 52 | expect(typeof getValidatedOptions === 'function'); 53 | const theFunction = () => { 54 | return getValidatedOptions({ links: ['a', true, 'false'] }, 'pluginName.options'); 55 | }; 56 | 57 | expect(theFunction).toThrowError(/(pluginName.options.links items must be an object or string)/); 58 | done(); 59 | }); 60 | 61 | it('should throw with the right error for links with external', done => { 62 | const { getValidatedOptions } = HtmlWebpackTagsPlugin.api; 63 | expect(typeof getValidatedOptions === 'function'); 64 | const theFunction = () => { 65 | return getValidatedOptions({ links: ['a', { path: 'b', external: { packageName: 'b', variableName: 'B' } }, 'c'] }, 'pluginName.options'); 66 | }; 67 | 68 | expect(theFunction).toThrowError(/(pluginName.options.links.external should not be used on non script tags)/); 69 | done(); 70 | }); 71 | 72 | it('should throw with the right error for scripts with bad external', done => { 73 | const { getValidatedOptions } = HtmlWebpackTagsPlugin.api; 74 | expect(typeof getValidatedOptions === 'function'); 75 | const theFunction = () => { 76 | return getValidatedOptions({ scripts: ['a', { path: 'b', external: 'abc' }, 'c'] }, 'pluginName.options'); 77 | }; 78 | 79 | expect(theFunction).toThrowError(/(pluginName.options.scripts.external should be an object)/); 80 | done(); 81 | }); 82 | 83 | it('should return the right options for valid options', done => { 84 | const { getValidatedOptions } = HtmlWebpackTagsPlugin.api; 85 | expect(typeof getValidatedOptions === 'function'); 86 | const theFunction = () => { 87 | return getValidatedOptions({ append: false, links: ['a', 'b', 'c'], scripts: [] }, 'pluginName.options', {}); 88 | }; 89 | 90 | expect(theFunction).not.toThrowError(); 91 | const result = theFunction(); 92 | expect(result.links).toEqual([ 93 | { path: 'a' }, 94 | { path: 'b' }, 95 | { path: 'c' } 96 | ]); 97 | expect(result.scripts).toEqual([]); 98 | done(); 99 | }); 100 | 101 | it('should return the right options for scripts with valid external', done => { 102 | const { getValidatedOptions } = HtmlWebpackTagsPlugin.api; 103 | expect(typeof getValidatedOptions === 'function'); 104 | const scriptsExpected = [ 105 | { path: 'a' }, 106 | { path: 'b', external: { variableName: 'B', packageName: 'b' } }, 107 | { path: 'c' } 108 | ]; 109 | const theFunction = () => { 110 | return getValidatedOptions({ scripts: ['a', { path: 'b', external: { variableName: 'B', packageName: 'b' } }, 'c'] }, 'pluginName.options', { append: false }); 111 | }; 112 | 113 | expect(theFunction).not.toThrowError(); 114 | expect(theFunction()).toEqual({ append: false, scripts: scriptsExpected, scriptsPrepend: scriptsExpected, scriptsAppend: [] }); 115 | done(); 116 | }); 117 | 118 | it('should return the passthrough options for valid options', done => { 119 | const { getValidatedOptions } = HtmlWebpackTagsPlugin.api; 120 | expect(typeof getValidatedOptions === 'function'); 121 | const scripts = ['a', { path: 'b', bar: '456', external: { variableName: 'B', packageName: 'b' } }, 'c']; 122 | const scriptsExpected = [ 123 | { path: 'a' }, 124 | { path: 'b', bar: '456', external: { variableName: 'B', packageName: 'b' } }, 125 | { path: 'c' } 126 | ]; 127 | const theFunction = () => { 128 | return getValidatedOptions({ scripts, foo: '123' }, 'pluginName.options', { append: false }); 129 | }; 130 | 131 | expect(theFunction).not.toThrowError(); 132 | expect(theFunction()).toEqual({ append: false, foo: '123', scripts: scriptsExpected, scriptsPrepend: scriptsExpected, scriptsAppend: [] }); 133 | done(); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /spec/browser.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const https = require('https'); 5 | const http = require('http'); 6 | 7 | require('jasmine-expect'); 8 | const { addMatchers } = require('add-matchers'); 9 | 10 | const matchersByName = { 11 | toBeTag (tagProperties, actual) { 12 | const node = actual.length > 0 ? actual[0] : actual; 13 | if (!node || node.tagName !== tagProperties.tagName) { 14 | return false; 15 | } 16 | if (tagProperties.attributes) { 17 | const tagAttrs = tagProperties.attributes; 18 | const nodeAttrs = node.attribs || {}; 19 | return !Object.keys(tagAttrs).some(tagAttr => tagAttrs[tagAttr] !== nodeAttrs[tagAttr]); 20 | } else { 21 | return true; 22 | } 23 | } 24 | }; 25 | 26 | addMatchers(matchersByName); 27 | 28 | const puppeteer = require('puppeteer'); 29 | const express = require('express'); 30 | const cheerio = require('cheerio'); 31 | const webpack = require('webpack'); 32 | const rimraf = require('rimraf'); 33 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 34 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 35 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 36 | const HtmlWebpackTagsPlugin = require('../'); 37 | 38 | const SERVER_HOST = 'localhost'; 39 | const SERVER_PORT = '9119'; 40 | 41 | const OUTPUT_FILENAME = '[name].js'; 42 | 43 | const EXTERNALS_OUTPUT_DIR = path.join(__dirname, 'dist'); 44 | 45 | const EXTERNALS_PATH = path.join(__dirname, 'fixtures', 'external'); 46 | const EXTERNALS_MODULES_PATH = path.join(EXTERNALS_PATH, 'node_modules'); 47 | const EXTERNALS_ENTRY = path.join(EXTERNALS_PATH, 'external-entry.js'); 48 | const EXTERNALS_STYLE = path.join(EXTERNALS_PATH, 'external-style.css'); 49 | const EXTERNALS_TEMPLATE_FILE = path.join(EXTERNALS_PATH, 'index.html'); 50 | 51 | const WEBPACK_CSS_RULE = { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] }; 52 | 53 | const WEBPACK_ENTRY = { 54 | app: EXTERNALS_ENTRY, 55 | style: EXTERNALS_STYLE 56 | }; 57 | 58 | const WEBPACK_OUTPUT = { 59 | path: EXTERNALS_OUTPUT_DIR, 60 | filename: OUTPUT_FILENAME 61 | }; 62 | 63 | const WEBPACK_MODULE = { 64 | rules: [WEBPACK_CSS_RULE] 65 | }; 66 | 67 | const createWebpackConfig = ({ 68 | webpackEntry, 69 | webpackOutput, 70 | webpackPublicPath, 71 | copyOptions, 72 | htmlOptions, 73 | options, 74 | minimize = true 75 | }) => { 76 | const createTagsPlugin = opts => new HtmlWebpackTagsPlugin(opts); 77 | 78 | const copyPlugins = copyOptions ? [new CopyWebpackPlugin({ patterns: copyOptions })] : []; 79 | const htmlPlugins = htmlOptions !== false ? [new HtmlWebpackPlugin(htmlOptions)] : []; 80 | const tagsPlugins = Array.isArray(options) ? options.map(createTagsPlugin) : options !== false ? [createTagsPlugin(options)] : []; 81 | 82 | return { 83 | entry: { 84 | ...WEBPACK_ENTRY, 85 | ...(webpackEntry !== undefined ? { app: webpackEntry } : {}) 86 | }, 87 | output: { 88 | ...WEBPACK_OUTPUT, 89 | ...(webpackOutput !== undefined ? { path: webpackOutput } : {}), 90 | ...(webpackPublicPath !== undefined ? { publicPath: webpackPublicPath } : {}) 91 | }, 92 | module: { ...WEBPACK_MODULE }, 93 | plugins: [ 94 | new MiniCssExtractPlugin({ filename: '[name].css' }), 95 | ...copyPlugins, 96 | ...htmlPlugins, 97 | ...tagsPlugins 98 | ], 99 | optimization: { 100 | minimize 101 | } 102 | }; 103 | }; 104 | 105 | async function startServer ({ serverPort, secure = false, path = EXTERNALS_OUTPUT_DIR }) { 106 | let theServer; 107 | function closeServer () { 108 | return new Promise((resolve, reject) => { 109 | try { 110 | theServer.close(() => resolve()); 111 | } catch (error) { 112 | reject(error); 113 | } 114 | }); 115 | } 116 | const app = express(); 117 | app.use(express.static(path)); 118 | 119 | if (secure) { 120 | const { keyPath, certPath } = secure; 121 | theServer = https.createServer({ key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) }, app); 122 | } else { 123 | theServer = http.createServer(app); 124 | } 125 | 126 | return new Promise((resolve, reject) => { 127 | theServer = theServer.listen(serverPort, error => { 128 | if (error) { 129 | reject(error); 130 | } else { 131 | resolve({ closeServer }); 132 | } 133 | }); 134 | }); 135 | } 136 | 137 | async function getBrowserContent (options) { 138 | const { serverHost, serverPort, waitForSelector } = options; 139 | const { closeServer } = await startServer(options); 140 | const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); 141 | const page = await browser.newPage(); 142 | const errors = []; 143 | page.on('pageerror', err => { 144 | errors.push(err); 145 | }); 146 | await page.goto('http://' + serverHost + ':' + serverPort); 147 | if (waitForSelector !== undefined) { 148 | await page.waitForSelector(waitForSelector); 149 | } 150 | const content = await page.content(); 151 | await browser.close(); 152 | await closeServer(); 153 | return { 154 | content, 155 | errors 156 | }; 157 | } 158 | 159 | describe('browser', () => { 160 | beforeEach(done => { 161 | rimraf(EXTERNALS_OUTPUT_DIR, done); 162 | }); 163 | 164 | it('should render correctly to the browser when an external script is used and append is set to false', done => { 165 | webpack(createWebpackConfig({ 166 | copyOptions: [{ from: path.join(EXTERNALS_MODULES_PATH, 'fake-b-package', 'fake-b-bundle.js'), to: 'fake-b-bundle.js' }], 167 | htmlOptions: { 168 | template: EXTERNALS_TEMPLATE_FILE 169 | }, 170 | options: { 171 | scripts: { 172 | path: 'fake-b-bundle.js', 173 | external: { 174 | packageName: 'fake-b-package', 175 | variableName: 'FakeB' 176 | } 177 | }, 178 | links: { 179 | path: 'data:;base64,=', 180 | attributes: { 181 | rel: 'icon' 182 | } 183 | }, 184 | append: false, 185 | publicPath: false, 186 | hash: false 187 | } 188 | }), (err, result) => { 189 | expect(err).toBeFalsy(); 190 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 191 | 192 | getBrowserContent({ serverHost: SERVER_HOST, serverPort: SERVER_PORT }) 193 | .then(({ content, errors }) => { 194 | expect(errors.length).toBe(0); 195 | const $ = cheerio.load(content); 196 | const divs = $('div.fake'); 197 | 198 | expect(divs.length).toBe(3); 199 | expect($(divs.get(0)).contents().toString()).toBe('% webpack fakeA %'); 200 | expect($(divs.get(1)).contents().toString()).toBe('% external fakeB %'); 201 | expect($(divs.get(2)).contents().toString()).toBe('% webpack fakeC % - depends on - % external fakeB %'); 202 | 203 | done(); 204 | }); 205 | }); 206 | }); 207 | 208 | it('should throw an error in the browser when an external script is used and append is set to true', done => { 209 | webpack(createWebpackConfig({ 210 | copyOptions: [{ from: path.join(EXTERNALS_MODULES_PATH, 'fake-b-package', 'fake-b-bundle.js'), to: 'fake-b-bundle.js' }], 211 | htmlOptions: { 212 | template: EXTERNALS_TEMPLATE_FILE 213 | }, 214 | options: { 215 | scripts: { 216 | path: 'fake-b-bundle.js', 217 | external: { 218 | packageName: 'fake-b-package', 219 | variableName: 'FakeB' 220 | } 221 | }, 222 | links: { 223 | path: 'data:;base64,=', 224 | attributes: { 225 | rel: 'icon' 226 | } 227 | }, 228 | append: true, 229 | prependExternals: false, 230 | publicPath: false, 231 | hash: false 232 | } 233 | }), (err, result) => { 234 | expect(err).toBeFalsy(); 235 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 236 | 237 | getBrowserContent({ serverHost: SERVER_HOST, serverPort: SERVER_PORT }) 238 | .then(({ content, errors }) => { 239 | expect(errors.length).toBe(1); 240 | expect(errors[0].message).toContain('FakeB is not defined'); 241 | 242 | const $ = cheerio.load(content); 243 | const divs = $('div.fake'); 244 | expect(divs.length).toBe(0); 245 | 246 | done(); 247 | }); 248 | }); 249 | }); 250 | 251 | it('should render correctly to the browser when a non-webpack external script is used with append set to false', done => { 252 | webpack(createWebpackConfig({ 253 | copyOptions: [ 254 | { 255 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-b-package', 'fake-b-bundle.js'), 256 | to: 'fake-b-bundle.js' 257 | }, 258 | { 259 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-other-package', 'fake-other-bundle.js'), 260 | to: 'fake-other-bundle.js' 261 | } 262 | ], 263 | htmlOptions: { 264 | template: EXTERNALS_TEMPLATE_FILE 265 | }, 266 | options: [ 267 | { 268 | scripts: [ 269 | { 270 | path: 'fake-b-bundle.js', 271 | external: { 272 | packageName: 'fake-b-package', 273 | variableName: 'FakeB' 274 | } 275 | } 276 | ], 277 | links: { 278 | path: 'data:;base64,=', 279 | attributes: { 280 | rel: 'icon' 281 | } 282 | }, 283 | append: false, 284 | prependExternals: false, 285 | publicPath: false, 286 | hash: false 287 | }, 288 | { 289 | scripts: { 290 | path: 'fake-other-bundle.js', 291 | external: { 292 | packageName: 'fake-other-package', 293 | variableName: 'FakeOther' 294 | } 295 | }, 296 | append: false, 297 | prependExternals: false, 298 | publicPath: false, 299 | hash: false 300 | } 301 | ] 302 | }), (err, result) => { 303 | expect(err).toBeFalsy(); 304 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 305 | 306 | getBrowserContent({ 307 | serverHost: SERVER_HOST, 308 | serverPort: SERVER_PORT 309 | // waitForSelector: '.fake-other' 310 | }) 311 | .then(({ content, errors }) => { 312 | expect(errors.length).toBe(0); 313 | const $ = cheerio.load(content); 314 | const divs = $('div.fake'); 315 | 316 | expect(divs.length).toBe(3); 317 | expect($(divs.get(0)).contents().toString()).toBe('% webpack fakeA %'); 318 | expect($(divs.get(1)).contents().toString()).toBe('% external fakeB %'); 319 | expect($(divs.get(2)).contents().toString()).toBe('% webpack fakeC % - depends on - % external fakeB %'); 320 | 321 | done(); 322 | }); 323 | }); 324 | }); 325 | 326 | it('should render correctly to the browser when a non-webpack external script is used with append set to true', done => { 327 | webpack(createWebpackConfig({ 328 | copyOptions: [ 329 | { 330 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-b-package', 'fake-b-bundle.js'), 331 | to: 'fake-b-bundle.js' 332 | }, 333 | { 334 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-other-package', 'fake-other-bundle.js'), 335 | to: 'fake-other-bundle.js' 336 | } 337 | ], 338 | htmlOptions: { 339 | template: EXTERNALS_TEMPLATE_FILE 340 | }, 341 | options: [ 342 | { 343 | scripts: [ 344 | { 345 | path: 'fake-b-bundle.js', 346 | external: { 347 | packageName: 'fake-b-package', 348 | variableName: 'FakeB' 349 | } 350 | } 351 | ], 352 | links: { 353 | path: 'data:;base64,=', 354 | attributes: { 355 | rel: 'icon' 356 | } 357 | }, 358 | prependExternals: false, 359 | append: false, 360 | publicPath: false, 361 | hash: false 362 | }, 363 | { 364 | scripts: { 365 | path: 'fake-other-bundle.js', 366 | external: { 367 | packageName: 'fake-other-package', 368 | variableName: 'FakeOther' 369 | } 370 | }, 371 | prependExternals: false, 372 | append: true, 373 | publicPath: false, 374 | hash: false 375 | } 376 | ] 377 | }), (err, result) => { 378 | expect(err).toBeFalsy(); 379 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 380 | 381 | getBrowserContent({ 382 | serverHost: SERVER_HOST, 383 | serverPort: SERVER_PORT 384 | // waitForSelector: '.fake-other' 385 | }) 386 | .then(({ content, errors }) => { 387 | expect(errors.length).toBe(0); 388 | const $ = cheerio.load(content); 389 | const divs = $('div.fake'); 390 | 391 | expect(divs.length).toBe(1); 392 | expect($(divs.get(0)).contents().toString()).toBe('% fakeOther %'); 393 | 394 | done(); 395 | }); 396 | }); 397 | }); 398 | 399 | it('should render correctly to the browser when two external scripts are used', done => { 400 | webpack(createWebpackConfig({ 401 | copyOptions: [ 402 | { 403 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-b-package', 'fake-b-bundle.js'), 404 | to: 'fake-b-bundle.js' 405 | }, 406 | { 407 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-a-package', 'fake-a-bundle.js'), 408 | to: 'fake-a-bundle.js' 409 | } 410 | ], 411 | htmlOptions: { 412 | template: EXTERNALS_TEMPLATE_FILE 413 | }, 414 | options: { 415 | scripts: [ 416 | { 417 | path: 'fake-b-bundle.js', 418 | external: { 419 | packageName: 'fake-b-package', 420 | variableName: 'FakeB' 421 | } 422 | }, 423 | { 424 | path: 'fake-a-bundle.js', 425 | external: { 426 | packageName: 'fake-a-package', 427 | variableName: 'FakeA' 428 | } 429 | } 430 | ], 431 | links: { 432 | path: 'data:;base64,=', 433 | attributes: { 434 | rel: 'icon' 435 | } 436 | }, 437 | append: false, 438 | publicPath: false, 439 | hash: false 440 | } 441 | }), (err, result) => { 442 | expect(err).toBeFalsy(); 443 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 444 | 445 | getBrowserContent({ serverHost: SERVER_HOST, serverPort: SERVER_PORT }) 446 | .then(({ content, errors }) => { 447 | expect(errors.length).toBe(0); 448 | const $ = cheerio.load(content); 449 | const divs = $('div.fake'); 450 | 451 | expect(divs.length).toBe(3); 452 | expect($(divs.get(0)).contents().toString()).toBe('% external fakeA %'); 453 | expect($(divs.get(1)).contents().toString()).toBe('% external fakeB %'); 454 | expect($(divs.get(2)).contents().toString()).toBe('% webpack fakeC % - depends on - % external fakeB %'); 455 | 456 | done(); 457 | }); 458 | }); 459 | }); 460 | 461 | it('should render correctly to the browser when two dependency linked scripts are used', done => { 462 | webpack(createWebpackConfig({ 463 | copyOptions: [ 464 | { 465 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-b-package', 'fake-b-bundle.js'), 466 | to: 'fake-b-bundle.js' 467 | }, 468 | { 469 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-c-package', 'fake-c-bundle.js'), 470 | to: 'fake-c-bundle.js' 471 | } 472 | ], 473 | htmlOptions: { 474 | template: EXTERNALS_TEMPLATE_FILE 475 | }, 476 | options: { 477 | scripts: [ 478 | { 479 | path: 'fake-b-bundle.js', 480 | external: { 481 | packageName: 'fake-b-package', 482 | variableName: 'FakeB' 483 | } 484 | }, 485 | { 486 | path: 'fake-c-bundle.js', 487 | external: { 488 | packageName: 'fake-c-package', 489 | variableName: 'FakeC' 490 | } 491 | } 492 | ], 493 | links: { 494 | path: 'data:;base64,=', 495 | attributes: { 496 | rel: 'icon' 497 | } 498 | }, 499 | append: false, 500 | publicPath: false, 501 | hash: false 502 | } 503 | }), (err, result) => { 504 | expect(err).toBeFalsy(); 505 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 506 | 507 | getBrowserContent({ serverHost: SERVER_HOST, serverPort: SERVER_PORT }) 508 | .then(({ content, errors }) => { 509 | expect(errors.length).toBe(0); 510 | const $ = cheerio.load(content); 511 | const divs = $('div.fake'); 512 | 513 | expect(divs.length).toBe(3); 514 | expect($(divs.get(0)).contents().toString()).toBe('% webpack fakeA %'); 515 | expect($(divs.get(1)).contents().toString()).toBe('% external fakeB %'); 516 | expect($(divs.get(2)).contents().toString()).toBe('% external fakeC % - depends on - % external fakeB %'); 517 | 518 | done(); 519 | }); 520 | }); 521 | }); 522 | 523 | it('should throw an error in the browser when two dependency linked scripts are specified in the wrong order', done => { 524 | webpack(createWebpackConfig({ 525 | copyOptions: [ 526 | { 527 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-b-package', 'fake-b-bundle.js'), 528 | to: 'fake-b-bundle.js' 529 | }, 530 | { 531 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-c-package', 'fake-c-bundle.js'), 532 | to: 'fake-c-bundle.js' 533 | } 534 | ], 535 | htmlOptions: { 536 | template: EXTERNALS_TEMPLATE_FILE 537 | }, 538 | options: { 539 | scripts: [ 540 | { 541 | path: 'fake-c-bundle.js', 542 | external: { 543 | packageName: 'fake-c-package', 544 | variableName: 'FakeC' 545 | } 546 | }, 547 | { 548 | path: 'fake-b-bundle.js', 549 | external: { 550 | packageName: 'fake-b-package', 551 | variableName: 'FakeB' 552 | } 553 | } 554 | ], 555 | links: { 556 | path: 'data:;base64,=', 557 | attributes: { 558 | rel: 'icon' 559 | } 560 | }, 561 | append: false, 562 | publicPath: false, 563 | hash: false 564 | } 565 | }), (err, result) => { 566 | expect(err).toBeFalsy(); 567 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 568 | 569 | getBrowserContent({ serverHost: SERVER_HOST, serverPort: SERVER_PORT }) 570 | .then(({ content, errors }) => { 571 | expect(errors.length).toBe(1); 572 | expect(errors[0].message).toContain('Cannot read propert'); 573 | expect(errors[0].message).toContain('\'fakeB\''); 574 | expect(errors[0].message).toContain('of undefined'); 575 | 576 | const $ = cheerio.load(content); 577 | const divs = $('div.fake'); 578 | expect(divs.length).toBe(0); 579 | 580 | done(); 581 | }); 582 | }); 583 | }); 584 | 585 | it('should throw an error the browser when one linked script is used that is dependent on a webpack bundled package and append is set to false', done => { 586 | webpack(createWebpackConfig({ 587 | copyOptions: [{ 588 | from: path.join(EXTERNALS_MODULES_PATH, 'fake-c-package', 'fake-c-bundle.js'), 589 | to: 'fake-c-bundle.js' 590 | }], 591 | htmlOptions: { 592 | template: EXTERNALS_TEMPLATE_FILE 593 | }, 594 | options: { 595 | scripts: [ 596 | { 597 | path: 'fake-c-bundle.js', 598 | external: { 599 | packageName: 'fake-c-package', 600 | variableName: 'FakeC' 601 | } 602 | } 603 | ], 604 | links: { 605 | path: 'data:;base64,=', 606 | attributes: { 607 | rel: 'icon' 608 | } 609 | }, 610 | append: false, 611 | publicPath: false, 612 | hash: false 613 | } 614 | }), (err, result) => { 615 | expect(err).toBeFalsy(); 616 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 617 | 618 | getBrowserContent({ serverHost: SERVER_HOST, serverPort: SERVER_PORT }) 619 | .then(({ content, errors }) => { 620 | expect(errors.length).toBe(1); 621 | expect(errors[0].message).toContain('Cannot read propert'); 622 | expect(errors[0].message).toContain('\'fakeB\''); 623 | expect(errors[0].message).toContain('of undefined'); 624 | 625 | const $ = cheerio.load(content); 626 | const divs = $('div.fake'); 627 | expect(divs.length).toBe(0); 628 | 629 | done(); 630 | }); 631 | }); 632 | }); 633 | }); 634 | -------------------------------------------------------------------------------- /spec/fixtures/a-dir/file-a.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/a-dir/file-a.css -------------------------------------------------------------------------------- /spec/fixtures/a-dir/file-a.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/a-dir/file-a.js -------------------------------------------------------------------------------- /spec/fixtures/a-dir/file-b.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/a-dir/file-b.css -------------------------------------------------------------------------------- /spec/fixtures/a-dir/file-b.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/a-dir/file-b.js -------------------------------------------------------------------------------- /spec/fixtures/app.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 24px; 3 | } -------------------------------------------------------------------------------- /spec/fixtures/astyle.css: -------------------------------------------------------------------------------- 1 | h2 { 2 | font-size: 20px; 3 | } -------------------------------------------------------------------------------- /spec/fixtures/entry.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/entry.js -------------------------------------------------------------------------------- /spec/fixtures/exclude.css: -------------------------------------------------------------------------------- 1 | .exclude { 2 | font-size: 24px; 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/external/external-entry.js: -------------------------------------------------------------------------------- 1 | const fakeA = require('fake-a-package').fakeA; 2 | const fakeB = require('fake-b-package').fakeB; 3 | const fakeC = require('fake-c-package').fakeC; 4 | 5 | const fakes = [fakeA, fakeB, fakeC]; 6 | 7 | const fakeResults = fakes.map(fake => fake()); 8 | 9 | const fakeResultsHTML = fakeResults.map(fakeResult => '
' + fakeResult + '
').join(''); 10 | 11 | document.getElementById('external-root').innerHTML = fakeResultsHTML; 12 | -------------------------------------------------------------------------------- /spec/fixtures/external/external-style.css: -------------------------------------------------------------------------------- 1 | h2 { 2 | font-size: 16px; 3 | } -------------------------------------------------------------------------------- /spec/fixtures/external/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | html-webpack-tags-plugin external browser test 8 | 9 | 10 | 11 |
12 | Loading... 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-a-package/fake-a-bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = global || self, factory(global.FakeA = {})); 5 | }(this, function (exports) { 'use strict'; 6 | 7 | function fakeA() { 8 | return '% external fakeA %'; 9 | } 10 | 11 | exports.fakeA = fakeA; 12 | 13 | Object.defineProperty(exports, '__esModule', { value: true }); 14 | 15 | })); 16 | //# sourceMappingURL=fake-a-bundle.js.map 17 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-a-package/fake-a-entry.js: -------------------------------------------------------------------------------- 1 | function fakeA () { 2 | return '% webpack fakeA %'; 3 | } 4 | 5 | module.exports = { 6 | fakeA: fakeA 7 | }; 8 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-a-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fake-a-package", 3 | "version": "1.0.0", 4 | "main": "fake-a-entry.js", 5 | "scripts": { 6 | 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/jharris4/html-webpack-tags-plugin.git" 11 | }, 12 | "author": "Jon Harris (https://github.com/jharris4)", 13 | "license": "MIT", 14 | "homepage": "https://github.com/jharris4/html-webpack-tags-plugin", 15 | "dependencies": { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-b-package/fake-b-bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = global || self, factory(global.FakeB = {})); 5 | }(this, function (exports) { 'use strict'; 6 | 7 | function fakeB() { 8 | return '% external fakeB %'; 9 | } 10 | 11 | exports.fakeB = fakeB; 12 | 13 | Object.defineProperty(exports, '__esModule', { value: true }); 14 | 15 | })); 16 | //# sourceMappingURL=fake-b-bundle.js.map 17 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-b-package/fake-b-entry.js: -------------------------------------------------------------------------------- 1 | function fakeB () { 2 | return '% webpack fakeB %'; 3 | } 4 | 5 | module.exports = { 6 | fakeB: fakeB 7 | }; 8 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-b-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fake-b-package", 3 | "version": "1.0.0", 4 | "main": "fake-b-entry.js", 5 | "scripts": { 6 | 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/jharris4/html-webpack-tags-plugin.git" 11 | }, 12 | "author": "Jon Harris (https://github.com/jharris4)", 13 | "license": "MIT", 14 | "homepage": "https://github.com/jharris4/html-webpack-tags-plugin", 15 | "dependencies": { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-c-package/fake-c-bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('fake-b-package')) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = global || self, factory(global.FakeC = {}, global.FakeB)); 5 | }(this, function (exports, FakeB) { 'use strict'; 6 | 7 | // var FakeB__default = 'default' in FakeB ? FakeB['default'] : FakeB; 8 | 9 | function fakeC() { 10 | return '% external fakeC % - depends on - ' + FakeB.fakeB(); 11 | } 12 | 13 | exports.fakeC = fakeC; 14 | 15 | Object.defineProperty(exports, '__esModule', { value: true }); 16 | 17 | })); 18 | // # sourceMappingURL=fake-c-bundle.js.map 19 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-c-package/fake-c-entry.js: -------------------------------------------------------------------------------- 1 | const { fakeB } = require('fake-b-package'); 2 | 3 | function fakeC () { 4 | return '% webpack fakeC % - depends on - ' + fakeB(); 5 | } 6 | 7 | module.exports = { 8 | fakeC: fakeC 9 | }; 10 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-c-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fake-c-package", 3 | "version": "1.0.0", 4 | "main": "fake-c-entry.js", 5 | "scripts": { 6 | 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/jharris4/html-webpack-tags-plugin.git" 11 | }, 12 | "author": "Jon Harris (https://github.com/jharris4)", 13 | "license": "MIT", 14 | "homepage": "https://github.com/jharris4/html-webpack-tags-plugin", 15 | "dependencies": { 16 | "fake-b-package": "1.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-other-package/fake-other-bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = global || self, factory(global.FakeOther = {})); 5 | }(this, function (exports) { 'use strict'; 6 | 7 | const rootElement = document.getElementById('external-root'); 8 | 9 | rootElement.innerHTML = '
% fakeOther %
'; 10 | 11 | })); 12 | //# sourceMappingURL=fake-b-bundle.js.map 13 | -------------------------------------------------------------------------------- /spec/fixtures/external/node_modules/fake-other-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fake-other-package", 3 | "version": "1.0.0", 4 | "main": "fake-other-bundle.js", 5 | "scripts": { 6 | 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/jharris4/html-webpack-tags-plugin.git" 11 | }, 12 | "author": "Jon Harris (https://github.com/jharris4)", 13 | "license": "MIT", 14 | "homepage": "https://github.com/jharris4/html-webpack-tags-plugin", 15 | "dependencies": { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /spec/fixtures/external/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-webpack-tags-plugin-test", 3 | "version": "1.0.0", 4 | "main": "browser-entry.js", 5 | "scripts": { 6 | 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/jharris4/html-webpack-tags-plugin.git" 11 | }, 12 | "keywords": [ 13 | "webpack", 14 | "plugin", 15 | "html-webpack-plugin", 16 | "html", 17 | "tags", 18 | "inject", 19 | "include", 20 | "assets" 21 | ], 22 | "author": "Jon Harris (https://github.com/jharris4)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/jharris4/html-webpack-tags-plugin/issues" 26 | }, 27 | "homepage": "https://github.com/jharris4/html-webpack-tags-plugin", 28 | "dependencies": { 29 | "fake-a-package": "1.0.0", 30 | "fake-b-package": "1.0.0", 31 | "fake-c-package": "1.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spec/fixtures/glob-a.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/glob-a.css -------------------------------------------------------------------------------- /spec/fixtures/glob-a.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/glob-a.js -------------------------------------------------------------------------------- /spec/fixtures/glob-b.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/glob-b.css -------------------------------------------------------------------------------- /spec/fixtures/glob-b.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/glob-b.js -------------------------------------------------------------------------------- /spec/fixtures/glob.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 36px; 3 | } -------------------------------------------------------------------------------- /spec/fixtures/glob.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/glob.js -------------------------------------------------------------------------------- /spec/fixtures/index-no-inject.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | html-webpack-tags-plugin 9 | 10 | <% for (var cssIndex = 0; cssIndex < htmlWebpackPlugin.files.css.length; cssIndex++) { %> 11 | 12 | <% } %> 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | Loading... 22 |
23 | 31 |
32 | 33 | <% for (var jsIndex = 0; jsIndex < htmlWebpackPlugin.files.js.length; jsIndex++) { %> 34 | 35 | <% } %> 36 | 37 | 38 | -------------------------------------------------------------------------------- /spec/fixtures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | html-webpack-tags-plugin 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | Loading... 19 |
20 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /spec/fixtures/other: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-tags-plugin/bf5952c14430e998bc097e987a99f03a8eb36033/spec/fixtures/other -------------------------------------------------------------------------------- /spec/option_validation.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | const path = require('path'); 3 | require('jasmine-expect'); 4 | 5 | const HtmlWebpackTagsPlugin = require('../'); 6 | 7 | const FIXTURES_PATH = path.join(__dirname, './fixtures'); 8 | 9 | describe('option validation', () => { 10 | it('should throw an error if no options are provided', done => { 11 | const theFunction = () => { 12 | return new HtmlWebpackTagsPlugin(); 13 | }; 14 | 15 | expect(theFunction).toThrowError(/(options should be an object)/); 16 | done(); 17 | }); 18 | 19 | it('should throw an error if the options are not an object', done => { 20 | const theFunction = () => { 21 | return new HtmlWebpackTagsPlugin('hello'); 22 | }; 23 | 24 | expect(theFunction).toThrowError(/(options should be an object)/); 25 | done(); 26 | }); 27 | 28 | it('should not throw an error if the options is an empty object', done => { 29 | const theFunction = () => { 30 | return new HtmlWebpackTagsPlugin({}); 31 | }; 32 | 33 | expect(theFunction).not.toThrowError(); 34 | done(); 35 | }); 36 | 37 | describe('options.jsExtensions', () => { 38 | it('should throw an error if the jsExtensions is not an array or string', done => { 39 | const theFunction = () => { 40 | return new HtmlWebpackTagsPlugin({ tags: [], append: false, jsExtensions: 123 }); 41 | }; 42 | expect(theFunction).toThrowError(/(options\.jsExtensions should be a string or array of strings)/); 43 | done(); 44 | }); 45 | 46 | it('should throw an error if any of the jsExtensions are not a string', done => { 47 | const theFunction = () => { 48 | return new HtmlWebpackTagsPlugin({ tags: [], append: false, jsExtensions: ['a', 123, 'b'] }); 49 | }; 50 | expect(theFunction).toThrowError(/(options\.jsExtensions array should only contain strings)/); 51 | done(); 52 | }); 53 | }); 54 | 55 | describe('options.cssExtensions', () => { 56 | it('should throw an error if the cssExtensions is not an array or string', done => { 57 | const theFunction = () => { 58 | return new HtmlWebpackTagsPlugin({ tags: [], append: false, cssExtensions: 123 }); 59 | }; 60 | expect(theFunction).toThrowError(/(options\.cssExtensions should be a string or array of strings)/); 61 | done(); 62 | }); 63 | 64 | it('should throw an error if any of the cssExtensions are not a string', done => { 65 | const theFunction = () => { 66 | return new HtmlWebpackTagsPlugin({ tags: [], append: false, cssExtensions: ['a', 123, 'b'] }); 67 | }; 68 | expect(theFunction).toThrowError(/(options\.cssExtensions array should only contain strings)/); 69 | done(); 70 | }); 71 | }); 72 | 73 | describe('options.append', () => { 74 | it('should not throw an error if the append flag is not provided', done => { 75 | const theFunction = () => { 76 | return new HtmlWebpackTagsPlugin({ tags: [] }); 77 | }; 78 | 79 | expect(theFunction).not.toThrowError(); 80 | done(); 81 | }); 82 | 83 | it('should throw an error if the append flag is not a boolean', done => { 84 | const theFunction = () => { 85 | return new HtmlWebpackTagsPlugin({ tags: [], append: 'hello' }); 86 | }; 87 | 88 | expect(theFunction).toThrowError(/(options.append should be a boolean)/); 89 | done(); 90 | }); 91 | }); 92 | 93 | describe('options.publicPath', () => { 94 | it('should throw an error if the publicPath option is not a boolean or string or a function', done => { 95 | const theFunction = () => { 96 | return new HtmlWebpackTagsPlugin({ publicPath: 123 }); 97 | }; 98 | 99 | expect(theFunction).toThrowError(/(options.publicPath should be a boolean or a string or a function)/); 100 | done(); 101 | }); 102 | 103 | it('should throw an error if the usePublicPath flag is not a boolean', done => { 104 | const theFunction = () => { 105 | return new HtmlWebpackTagsPlugin({ usePublicPath: 123 }); 106 | }; 107 | 108 | expect(theFunction).toThrowError(/(options.usePublicPath should be a boolean)/); 109 | done(); 110 | }); 111 | 112 | it('should throw an error if the addPublicPath option is not a function', done => { 113 | const theFunction = () => { 114 | return new HtmlWebpackTagsPlugin({ addPublicPath: 123 }); 115 | }; 116 | 117 | expect(theFunction).toThrowError(/(options.addPublicPath should be a function)/); 118 | done(); 119 | }); 120 | 121 | it('should throw an error if publicPath and usePublicPath are specified together', done => { 122 | const theFunction = () => { 123 | return new HtmlWebpackTagsPlugin({ publicPath: true, usePublicPath: false }); 124 | }; 125 | 126 | expect(theFunction).toThrowError(/(options.publicPath should not be used with either usePublicPath or addPublicPath)/); 127 | done(); 128 | }); 129 | 130 | it('should throw an error if publicPath and addPublicPath are specified together', done => { 131 | const theFunction = () => { 132 | return new HtmlWebpackTagsPlugin({ publicPath: true, addPublicPath: () => '' }); 133 | }; 134 | 135 | expect(theFunction).toThrowError(/(options.publicPath should not be used with either usePublicPath or addPublicPath)/); 136 | done(); 137 | }); 138 | }); 139 | 140 | describe('options.hash', () => { 141 | it('should throw an error if the hash option is not a boolean or string or a function', done => { 142 | const nonBooleanCheck = [123, /regex/, [], {}]; 143 | 144 | nonBooleanCheck.forEach(val => { 145 | const theCheck = () => { 146 | return new HtmlWebpackTagsPlugin({ tags: [], append: true, publicPath: true, hash: val }); 147 | }; 148 | expect(theCheck).toThrowError(/(options.hash should be a boolean or a string or a function that returns a string)/); 149 | }); 150 | done(); 151 | }); 152 | 153 | it('should throw an error if the hash is a number', done => { 154 | const theFunction = () => { 155 | return new HtmlWebpackTagsPlugin({ hash: 123 }); 156 | }; 157 | 158 | expect(theFunction).toThrowError(/(options.hash should be a boolean or a string or a function that returns a string)/); 159 | done(); 160 | }); 161 | 162 | it('should throw an error if the useHash flag is not a boolean', done => { 163 | const theFunction = () => { 164 | return new HtmlWebpackTagsPlugin({ useHash: 123 }); 165 | }; 166 | 167 | expect(theFunction).toThrowError(/(options.useHash should be a boolean)/); 168 | done(); 169 | }); 170 | 171 | it('should throw an error if the addHash option is not a function', done => { 172 | const theFunction = () => { 173 | return new HtmlWebpackTagsPlugin({ addHash: 123 }); 174 | }; 175 | 176 | expect(theFunction).toThrowError(/(options.addHash should be a function that returns a string)/); 177 | done(); 178 | }); 179 | 180 | it('should throw an error if hash and useHash are specified together', done => { 181 | const theFunction = () => { 182 | return new HtmlWebpackTagsPlugin({ hash: true, useHash: false }); 183 | }; 184 | 185 | expect(theFunction).toThrowError(/(options.hash should not be used with either useHash or addHash)/); 186 | done(); 187 | }); 188 | 189 | it('should throw an error if hash and addHash are specified together', done => { 190 | const theFunction = () => { 191 | return new HtmlWebpackTagsPlugin({ hash: true, addHash: () => '' }); 192 | }; 193 | 194 | expect(theFunction).toThrowError(/(options.hash should not be used with either useHash or addHash)/); 195 | done(); 196 | }); 197 | }); 198 | 199 | describe('options.prependExternals', () => { 200 | it('should throw an error if prependExternals is not a boolean', done => { 201 | const nonBooleanCheck = [123, 'true', /regex/, {}]; 202 | 203 | nonBooleanCheck.forEach(val => { 204 | const theCheck = () => { 205 | return new HtmlWebpackTagsPlugin({ prependExternals: val }); 206 | }; 207 | 208 | expect(theCheck).toThrowError(/(options\.prependExternals should be a boolean)/); 209 | }); 210 | 211 | done(); 212 | }); 213 | 214 | it('should not throw an error if prependExternals is true', done => { 215 | const nonStringCheck = [123, true, /regex/, {}]; 216 | 217 | nonStringCheck.forEach(val => { 218 | const theCheck = () => { 219 | return new HtmlWebpackTagsPlugin({ prependExternals: true }); 220 | }; 221 | 222 | expect(theCheck).not.toThrowError(); 223 | }); 224 | 225 | done(); 226 | }); 227 | 228 | it('should not throw an error if prependExternals is false', done => { 229 | const nonStringCheck = [123, true, /regex/, {}]; 230 | 231 | nonStringCheck.forEach(val => { 232 | const theCheck = () => { 233 | return new HtmlWebpackTagsPlugin({ prependExternals: false }); 234 | }; 235 | 236 | expect(theCheck).not.toThrowError(); 237 | }); 238 | 239 | done(); 240 | }); 241 | }); 242 | 243 | describe('options.files', () => { 244 | it('should throw an error if the files option is not a string', done => { 245 | const nonStringCheck = [123, true, /regex/, {}]; 246 | 247 | nonStringCheck.forEach(val => { 248 | const theCheck = () => { 249 | return new HtmlWebpackTagsPlugin({ tags: [], append: true, publicPath: true, files: val }); 250 | }; 251 | 252 | expect(theCheck).toThrowError(/(options\.files should be a string or array of strings)/); 253 | }); 254 | 255 | done(); 256 | }); 257 | 258 | it('should throw an error if any of the files options are not strings', done => { 259 | const theFunction = () => { 260 | return new HtmlWebpackTagsPlugin({ tags: ['foo.js', 'bar.css'], append: false, files: ['abc', true, 'def'] }); 261 | }; 262 | expect(theFunction).toThrowError(/(options\.files should be a string or array of strings)/); 263 | done(); 264 | }); 265 | }); 266 | 267 | describe('options.metas', () => { 268 | it('should throw an error if metas is a string', done => { 269 | const theFunction = () => { 270 | return new HtmlWebpackTagsPlugin({ metas: 'a string' }); 271 | }; 272 | 273 | expect(theFunction).toThrowError(/(options.metas should be an object or array)/); 274 | done(); 275 | }); 276 | 277 | it('should throw an error if metas is an object without attributes', done => { 278 | const theFunction = () => { 279 | return new HtmlWebpackTagsPlugin({ metas: { path: 'abc' } }); 280 | }; 281 | 282 | expect(theFunction).toThrowError(/(options.metas object must have an object attributes property)/); 283 | done(); 284 | }); 285 | 286 | it('should throw an error if metas is an object with non string path', done => { 287 | const theFunction = () => { 288 | return new HtmlWebpackTagsPlugin({ metas: { attributes: { a: 'b' }, path: 123 } }); 289 | }; 290 | 291 | expect(theFunction).toThrowError(/(options.metas object should have a string path property)/); 292 | done(); 293 | }); 294 | 295 | it('should throw an error if metas is an array containing a string', done => { 296 | const theFunction = () => { 297 | return new HtmlWebpackTagsPlugin({ metas: [{ attributes: { a: 1 }, path: 'a' }, '', { attributes: { b: 2 }, path: 'b' }] }); 298 | }; 299 | 300 | expect(theFunction).toThrowError(/(options.metas items must be an object)/); 301 | done(); 302 | }); 303 | 304 | it('should throw an error if metas is an object with empty attributes', done => { 305 | const theFunction = () => { 306 | return new HtmlWebpackTagsPlugin({ metas: { attributes: { }, path: 'b' } }); 307 | }; 308 | 309 | expect(theFunction).toThrowError(/(options.metas object must have a non empty object attributes property)/); 310 | done(); 311 | }); 312 | 313 | it('should throw an error if metas is an array containing an object with empty attributes', done => { 314 | const theFunction = () => { 315 | return new HtmlWebpackTagsPlugin({ metas: [{ attributes: { a: 1 }, path: 'a' }, { attributes: { }, path: 'b' }] }); 316 | }; 317 | 318 | expect(theFunction).toThrowError(/(options.metas object must have a non empty object attributes property)/); 319 | done(); 320 | }); 321 | 322 | it('should throw an error if metas has glob without path', done => { 323 | const theFunction = () => { 324 | return new HtmlWebpackTagsPlugin({ metas: { attributes: { a: 1 }, glob: 'a' } }); 325 | }; 326 | 327 | expect(theFunction).toThrowError(/(options.metas object must have a path property when glob is used)/); 328 | done(); 329 | }); 330 | }); 331 | 332 | describe('options[tags|links|scripts]', () => { 333 | runTestsForOption('tags', false, runTestsForAssetType); 334 | runTestsForOption('tags', true, runTestsForAssetType); 335 | runTestsForOption('links', false); 336 | runTestsForOption('scripts', true); 337 | }); 338 | }); 339 | 340 | function runTestsForOption (optionName, isScript, runExtraTests) { 341 | const ext = isScript ? '.js' : '.css'; 342 | describe(`options.${optionName}`, () => { 343 | it(`should throw an error if the ${optionName} are not an array or string or object`, done => { 344 | const theFunction = () => { 345 | return new HtmlWebpackTagsPlugin({ [optionName]: 123 }); 346 | }; 347 | 348 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} should be a string, object, or array)`)); 349 | done(); 350 | }); 351 | 352 | it(`should throw an error if the ${optionName} contains objects and a boolean`, done => { 353 | const theFunction = () => { 354 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, false, { path: `b${ext}` }] }); 355 | }; 356 | 357 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} items must be an object or string)`)); 358 | done(); 359 | }); 360 | 361 | it(`should throw an error if the ${optionName} contains string and a boolean`, done => { 362 | const theFunction = () => { 363 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, true, `bar${ext}`] }); 364 | }; 365 | 366 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} items must be an object or string)`)); 367 | done(); 368 | }); 369 | 370 | it(`should not throw an error if the ${optionName} contains strings and objects`, done => { 371 | const theFunction = () => { 372 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `file${ext}` }, `bar${ext}`] }); 373 | }; 374 | 375 | expect(theFunction).not.toThrowError(); 376 | done(); 377 | }); 378 | }); 379 | 380 | describe(`options.${optionName} path`, () => { 381 | it(`should throw an error if the ${optionName} contains an element that is an empty object`, done => { 382 | const theFunction = () => { 383 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, {}, { path: `b${ext}` }] }); 384 | }; 385 | 386 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object must have a string path property)`)); 387 | done(); 388 | }); 389 | 390 | it(`should throw an error if the ${optionName} contains an element that is an object with a non string path`, done => { 391 | const theFunction = () => { 392 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: 123, type: 'js' }, { path: `c${ext}` }] }); 393 | }; 394 | 395 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object must have a string path property)`)); 396 | done(); 397 | }); 398 | 399 | it(`should not throw an error if the ${optionName} contains elements that are all objects that have a path`, done => { 400 | const theFunction = () => { 401 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}` }, { path: `c${ext}` }] }); 402 | }; 403 | 404 | expect(theFunction).not.toThrowError(); 405 | done(); 406 | }); 407 | }); 408 | 409 | describe(`options.${optionName} append`, () => { 410 | it(`should throw an error if the ${optionName} contains an element that is an object with a non boolean append`, done => { 411 | const theFunction = () => { 412 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}`, append: 123 }, { path: `c${ext}` }] }); 413 | }; 414 | 415 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName}.append should be a boolean)`)); 416 | done(); 417 | }); 418 | 419 | it(`should not throw an error if the ${optionName} contains elements that are all objects that have a boolean append`, done => { 420 | const theFunction = () => { 421 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}`, append: true }, { path: `b${ext}`, append: false }, { path: `c${ext}`, append: true }] }); 422 | }; 423 | 424 | expect(theFunction).not.toThrowError(); 425 | done(); 426 | }); 427 | }); 428 | 429 | describe(`options.${optionName} publicPath`, () => { 430 | it(`should not throw an error if the ${optionName} contains an element that is an object with publicPath set to string`, done => { 431 | const theFunction = () => { 432 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}`, publicPath: 'my-public-path' }, { path: `c${ext}` }] }); 433 | }; 434 | 435 | expect(theFunction).not.toThrowError(); 436 | done(); 437 | }); 438 | 439 | it(`should throw an error if the ${optionName} contains an element that is an object with publicPath set to object`, done => { 440 | const theFunction = () => { 441 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}`, publicPath: {} }, { path: `c${ext}` }] }); 442 | }; 443 | 444 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName}.publicPath should be a boolean or a string or a function that returns a string)`)); 445 | done(); 446 | }); 447 | 448 | it(`should throw an error if the ${optionName} contains an element that is an object with publicPath set to number`, done => { 449 | const theFunction = () => { 450 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}`, publicPath: 0 }, { path: `c${ext}` }] }); 451 | }; 452 | 453 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName}.publicPath should be a boolean or a string or a function that returns a string)`)); 454 | done(); 455 | }); 456 | 457 | it(`should throw an error if the ${optionName} contains an element that is an object with publicPath set to array`, done => { 458 | const theFunction = () => { 459 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}`, publicPath: [] }, { path: `c${ext}` }] }); 460 | }; 461 | 462 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName}.publicPath should be a boolean or a string or a function that returns a string)`)); 463 | done(); 464 | }); 465 | 466 | it(`should not throw an error if the ${optionName} contains an element that is an object with publicPath set to true`, done => { 467 | const theFunction = () => { 468 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}`, publicPath: true }, { path: `b${ext}` }, { path: `c${ext}` }] }); 469 | }; 470 | 471 | expect(theFunction).not.toThrowError(); 472 | done(); 473 | }); 474 | }); 475 | 476 | describe(`options.${optionName} attributes`, () => { 477 | it(`should throw an error if the ${optionName} contains an element that is an object with non object string attributes`, done => { 478 | const theFunction = () => { 479 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}`, attributes: '' }, { path: `c${ext}` }] }); 480 | }; 481 | 482 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object should have an object attributes property)`)); 483 | done(); 484 | }); 485 | 486 | it(`should throw an error if the ${optionName} contains an element that is an object with array attributes`, done => { 487 | const theFunction = () => { 488 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}`, attributes: [] }, { path: `c${ext}` }] }); 489 | }; 490 | 491 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object should have an object attributes property)`)); 492 | done(); 493 | }); 494 | 495 | it(`should throw an error if the ${optionName} contains an element that is an object with number attributes`, done => { 496 | const theFunction = () => { 497 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}`, attributes: 0 }, { path: `c${ext}` }] }); 498 | }; 499 | 500 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object should have an object attributes property)`)); 501 | done(); 502 | }); 503 | 504 | it(`should throw an error if the ${optionName} contains an element that is an object with boolean attributes`, done => { 505 | const theFunction = () => { 506 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}`, attributes: true }, { path: `c${ext}` }] }); 507 | }; 508 | 509 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object should have an object attributes property)`)); 510 | done(); 511 | }); 512 | 513 | it(`should not throw an error if the ${optionName} contains an element that is an object with empty object attributes`, done => { 514 | const theFunction = () => { 515 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: `a${ext}` }, { path: `b${ext}`, attributes: {} }, { path: `c${ext}` }] }); 516 | }; 517 | 518 | expect(theFunction).not.toThrowError(); 519 | done(); 520 | }); 521 | 522 | it('should throw an error if any of the tags options are objects with an attributes property that is not an object', done => { 523 | const theFunction = () => { 524 | return new HtmlWebpackTagsPlugin({ tags: [`foo${ext}`, { path: `pathWithExtension${ext}`, attributes: 'foobar' }, `bar${ext}`] }); 525 | }; 526 | expect(theFunction).toThrowError(/(options\.tags object should have an object attributes property)/); 527 | done(); 528 | }); 529 | 530 | it('should throw an error if any of the tags options are objects with an attributes property with non string or boolean values', done => { 531 | const theFunction = () => { 532 | return new HtmlWebpackTagsPlugin({ tags: [`foo${ext}`, { path: `pathWithExtension${ext}`, attributes: { crossorigin: 'crossorigin', id: null, enabled: true } }, `bar${ext}`] }); 533 | }; 534 | expect(theFunction).toThrowError(/(options\.tags object attribute values should be strings, booleans or numbers)/); 535 | done(); 536 | }); 537 | 538 | it('should not throw an error if any of the tags options are objects with an attributes property with string or boolean values', done => { 539 | const theFunction = () => { 540 | return new HtmlWebpackTagsPlugin({ tags: [`foo${ext}`, { path: `pathWithExtension${ext}`, attributes: { crossorigin: 'crossorigin', id: 'test', enabled: true } }, `bar${ext}`] }); 541 | }; 542 | expect(theFunction).not.toThrowError(); 543 | done(); 544 | }); 545 | 546 | it('should not throw an error if any of the tags options are objects without an attributes property', done => { 547 | const theFunction = () => { 548 | return new HtmlWebpackTagsPlugin({ tags: [`foo${ext}`, { path: `pathWithExtension${ext}` }, `bar${ext}`] }); 549 | }; 550 | expect(theFunction).not.toThrowError(); 551 | done(); 552 | }); 553 | }); 554 | 555 | describe(`options.${optionName} glob`, () => { 556 | it(`should throw an error if any of the ${optionName} options are objects with a glob property that is not a string`, done => { 557 | const theFunction = () => { 558 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `a${ext}`, glob: 123, type: 'js' }, `bar${ext}`] }); 559 | }; 560 | 561 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object should have a string glob property)`)); 562 | done(); 563 | }); 564 | 565 | it(`should throw an error if any of the ${optionName} options are objects with a globPath property that is not a string`, done => { 566 | const theFunction = () => { 567 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `a${ext}`, globPath: 123, type: 'js' }, `bar${ext}`] }); 568 | }; 569 | 570 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object should have a string glob property)`)); 571 | done(); 572 | }); 573 | 574 | it(`should throw an error if any of the ${optionName} options are objects with a globFlatten property that is not a boolean`, done => { 575 | const theFunction = () => { 576 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: '', globPath: FIXTURES_PATH, glob: `*${ext}`, globFlatten: 123 }, `bar${ext}`] }); 577 | }; 578 | 579 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object should have a boolean globFlatten property)`)); 580 | done(); 581 | }); 582 | 583 | it(`should throw an error if any of the ${optionName} options are objects with glob specified but globPath missing`, done => { 584 | const theFunction = () => { 585 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `pathWithExtension${ext}`, glob: 'withoutExtensions*' }, `bar${ext}`], append: false }); 586 | }; 587 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object should have a string globPath property)`)); 588 | done(); 589 | }); 590 | 591 | it(`should throw an error if any of the ${optionName} options are objects with globPath specified but glob missing`, done => { 592 | const theFunction = () => { 593 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `pathWithExtension${ext}`, globPath: 'withoutExtensions*' }, `bar${ext}`], append: false }); 594 | }; 595 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object should have a string glob property)`)); 596 | done(); 597 | }); 598 | 599 | it(`should throw an error if any of the ${optionName} options are objects with glob that does not match any files`, done => { 600 | const theFunction = () => { 601 | return new HtmlWebpackTagsPlugin({ [optionName]: [{ path: 'assets/', globPath: FIXTURES_PATH, glob: `nonexistant*${ext}` }], append: true }); 602 | }; 603 | 604 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object glob found no files)`)); 605 | done(); 606 | }); 607 | }); 608 | 609 | describe(`options.${optionName} sourcePath`, () => { 610 | it(`should throw an error if any of the ${optionName} options are objects with an sourcePath property that is not a string`, done => { 611 | const theFunction = () => { 612 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `a${ext}`, sourcePath: 123, type: 'js' }, `bar${ext}`] }); 613 | }; 614 | 615 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName} object should have a string sourcePath property)`)); 616 | done(); 617 | }); 618 | }); 619 | 620 | describe(`options.${optionName} external`, () => { 621 | it(`should throw an error if any of the ${optionName} options are objects with external property that is not an object`, done => { 622 | const theFunction = () => { 623 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `a${ext}`, external: 123 }, `bar${ext}`] }); 624 | }; 625 | if (isScript) { 626 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName}.external should be an object)`)); 627 | } else { 628 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName}.external should not be used on non script tags)`)); 629 | } 630 | done(); 631 | }); 632 | 633 | if (isScript) { 634 | it(`should not throw an error if any of the ${optionName} options are objects with valid external objects`, done => { 635 | const theFunction = () => { 636 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `a${ext}`, external: { packageName: 'a', variableName: 'A' } }, `bar${ext}`] }); 637 | }; 638 | expect(theFunction).not.toThrowError(); 639 | done(); 640 | }); 641 | 642 | it(`should throw an error if any of the ${optionName} options are objects with external that is an empty object`, done => { 643 | const theFunction = () => { 644 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `a${ext}`, external: { } }, `bar${ext}`] }); 645 | }; 646 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName}.external should have a string packageName and variableName property)`)); 647 | done(); 648 | }); 649 | 650 | it(`should throw an error if any of the ${optionName} options are objects with external that has packageName but not variableName string properties`, done => { 651 | const theFunction = () => { 652 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `a${ext}`, external: { packageName: 'a' } }, `bar${ext}`] }); 653 | }; 654 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName}.external should have a string variableName property)`)); 655 | done(); 656 | }); 657 | 658 | it(`should throw an error if any of the ${optionName} options are objects with external that has variableName but not packageName string properties`, done => { 659 | const theFunction = () => { 660 | return new HtmlWebpackTagsPlugin({ [optionName]: [`foo${ext}`, { path: `a${ext}`, external: { variableName: 'A' } }, `bar${ext}`] }); 661 | }; 662 | expect(theFunction).toThrowError(new RegExp(`(options.${optionName}.external should have a string packageName property)`)); 663 | done(); 664 | }); 665 | } 666 | }); 667 | 668 | if (runExtraTests) { 669 | runExtraTests(ext); 670 | } 671 | } 672 | 673 | function runTestsForAssetType (ext) { 674 | describe('options.tags type', () => { 675 | it('should throw an error if any of the tags options are objects with an invalid type property', done => { 676 | const theFunction = () => { 677 | return new HtmlWebpackTagsPlugin({ tags: [`foo${ext}`, { path: `baz${ext}`, type: 'foo' }, `bar${ext}`], append: false }); 678 | }; 679 | expect(theFunction).toThrowError(/(options\.tags type must be css or js \(foo\))/); 680 | done(); 681 | }); 682 | 683 | it('should throw an error if any of the tags options do not end with .css or .js', done => { 684 | const theFunction = () => { 685 | return new HtmlWebpackTagsPlugin({ tags: ['foo.css', 'bad.txt', 'bar.js'], append: false }); 686 | }; 687 | expect(theFunction).toThrowError(/(options\.tags could not determine asset type for \(bad\.txt\))/); 688 | done(); 689 | }); 690 | 691 | it('should throw an error if any of the tags options are objects without a type property that cannot be inferred from the path', done => { 692 | const theFunction = () => { 693 | return new HtmlWebpackTagsPlugin({ tags: [`foo${ext}`, { path: 'pathWithoutExtension' }, `bar${ext}`], append: false }); 694 | }; 695 | expect(theFunction).toThrowError(/(options\.tags could not determine asset type for \(pathWithoutExtension\))/); 696 | done(); 697 | }); 698 | 699 | it('should not throw an error if any of the tags options are objects without a type property that can be inferred from the path', done => { 700 | const theFunction = () => { 701 | return new HtmlWebpackTagsPlugin({ tags: [`foo${ext}`, { path: `pathWithExtension${ext}` }, `bar${ext}`], append: false }); 702 | }; 703 | expect(theFunction).not.toThrowError(); 704 | done(); 705 | }); 706 | 707 | it('should not throw an error if any of the tags options are objects without a type property that can be inferred from the glob', done => { 708 | const theFunction = () => { 709 | return new HtmlWebpackTagsPlugin({ tags: [`foo${ext}`, { path: '', globPath: FIXTURES_PATH, glob: `glo*${ext}` }, `bar${ext}`], append: false }); 710 | }; 711 | expect(theFunction).not.toThrowError(); 712 | done(); 713 | }); 714 | }); 715 | } 716 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": ["../node_modules/jasmine-expect/index.js"], 7 | "stopSpecOnExpectationFailure": false, 8 | "random": true 9 | } 10 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | import { Compiler } from 'webpack'; 2 | 3 | export = HtmlWebpackTagsPlugin; 4 | 5 | declare class HtmlWebpackTagsPlugin { 6 | constructor(options?: HtmlWebpackTagsPlugin.Options); 7 | 8 | apply(compiler: Compiler): void; 9 | } 10 | 11 | declare namespace HtmlWebpackTagsPlugin { 12 | type AddHashFunction = (assetPath: string, hash: string) => string; 13 | type AddPublicPathFunction = (assetPath: string, publicPath: string) => string; 14 | type TypeString = 'css' | 'js'; 15 | type AttributesObject = { [attributeName: string]: string | boolean | number }; 16 | 17 | interface CommonOptions { 18 | append?: boolean; 19 | useHash?: boolean; 20 | addHash?: AddHashFunction 21 | hash?: boolean | string | AddHashFunction; 22 | usePublicPath?: boolean; 23 | addPublicPath?: AddPublicPathFunction 24 | publicPath?: boolean | string | AddPublicPathFunction; 25 | } 26 | 27 | interface Options extends CommonOptions { 28 | append?: boolean; 29 | prependExternals?: boolean; 30 | jsExtensions?: string | string[]; 31 | cssExtensions?: string | string[]; 32 | files?: string | string[]; 33 | tags?: string | MaybeLinkTagOptions | MaybeScriptTagOptions | Array; 34 | links?: string | LinkTagOptions | Array; 35 | scripts?: string | ScriptTagOptions | Array; 36 | metas?: string | MetaTagOptions | Array; 37 | } 38 | 39 | interface ExternalObject { 40 | packageName: string; 41 | variableName: string; 42 | } 43 | 44 | interface BaseTagOptions extends CommonOptions { 45 | glob?: string; 46 | globPath?: string; 47 | globFlatten?: boolean; 48 | sourcePath?: string; 49 | } 50 | 51 | interface LinkTagOptions extends BaseTagOptions { 52 | path: string; 53 | attributes?: AttributesObject; 54 | } 55 | 56 | interface ScriptTagOptions extends BaseTagOptions { 57 | path: string; 58 | attributes?: AttributesObject; 59 | external?: ExternalObject 60 | } 61 | 62 | interface MaybeLinkTagOptions extends LinkTagOptions { 63 | type?: TypeString; 64 | } 65 | 66 | interface MaybeScriptTagOptions extends ScriptTagOptions { 67 | type?: TypeString; 68 | } 69 | 70 | interface MetaTagOptions extends BaseTagOptions { 71 | path?: string; 72 | attributes: AttributesObject; 73 | } 74 | } 75 | --------------------------------------------------------------------------------