├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── spec ├── build.js ├── fixtures ├── blank.html ├── entry.js ├── images │ ├── another-face.svg │ ├── face.svg │ ├── moods.svg │ └── not-an-svg.png ├── index-allow-from-url.html ├── index-inline-all.html ├── index-pre-emit.html ├── index-svgo.html ├── index.html ├── partial-pre-emit.html └── partial.html ├── index.spec.js ├── jasmine-allow-from-url.tests.js ├── jasmine-inline-all.tests.js ├── jasmine-svgo.tests.js ├── jasmine.tests.js ├── support └── jasmine.json ├── webpack.allow-from-url.config.js ├── webpack.base.config.js ├── webpack.inline-all.config.js ├── webpack.post-emit.config.js ├── webpack.pre-emit.config.js └── webpack.svgo.config.js /.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 | /node_modules/ 2 | /dist/ 3 | npm-debug.log 4 | .history 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '10' 5 | script: 6 | - npm test 7 | notifications: 8 | slack: 9 | on_pull_requests: false 10 | rooms: 11 | secure: bKiOrBe1gYfqC0q2DPz97UW+tY8UKzbpKo8W5o8rsm5Niq2pFBNoWjHTLGOsjUlPPkmkBK3xjr2RiWMM3CvIe4fm4o8aap2tubjd4pubqMfBqEDZWlOlOQI7ZirKBhAflXIAp+CEDl4up/QBnWpq8K11/xgYgGD2CNrvp4KPfiHZzmyxXmNqgU05rt75cPbd3cYpQ0BudwNIcbVlsVkvWYc/OG8Wl4ItRfcvIjnjTqNaWaasEvcm+ljPTJ00cbcFWJdwlqIkfrMufosdyEYGaSyxHfVo+JTlSh6NSTkMt9Go64/qjVxFtnBc8nhhFx88e1Nu1UiBCLu0TH9x4Tmk4lKxcrCIW6xbomiwhS578EWWbtRjXESvwOw1c7YFXg+T3dLOgp13FiGCOqNhz5J6uWSAcavU/T22RPyNPrEaFpiYrEz5qQmnFJjvHIjybr+xH6ePdLb/QtJG7mtWfLenJRD6AZhroAPLEVZdLJ++dEM9UMouEX0pUlNYEAulREFfVc1H0/l2kcE91ZA3mCIEH3L2mlbxqJW4DmfmOGatAqHVgnQv6YXsbXPEVK4xrYJUl+PkilXjjDzd4gxfPLT49NDCCl+ysYfEH+wItH2uJj2x9zj4/wYpigjucENzi6ERse5xx2jnQpCAAed2RXXXTX2h1caO+Hp8xr9UEuMnGfI= 12 | email: false 13 | deploy: 14 | provider: npm 15 | email: guy@thebigsurf.co.uk 16 | api_key: 17 | secure: IEwmUcnZAnPxuEk7hro8/cElZ/kXK6b9ubqJwdtbd/RSndeyR/uY5fJxuI4EYtTK+0BKdux9TUMfjkv1ex8fyIhBbRZ8dS3zRLRH+94ZP+8Jm1h9LSiPGfRnzKOQ4YaIloXRSzZJdeHgwHKC6TFrqjgW9B4Yl4NQA9Wrz4TDmU7qEOvKAUp/6eAhd3UZL3L6x4zFgwbt3nv3LJYiO+MX2eDtFutSHzJTrRNFdndPxChaev4W81E0OHo9HYngvoSbsWm217IpBL+9H69+NiLWrB87ttSqa0ujQKwsx6nkQ3cdoz+I8hl2GeSjiMAKOO2ZtWwUyIi9z49szLnWxIUO9tb0QknXBoCFTSh7Ky/ztxDcY5Syi3SGD1KGF8Pje6qc7KB4TXyQB5g2X65Lk6+d8AKJRJDGIz8YJPQQutUt7D5Mu+3TVvfCAsI3kO0/xJK7YDQGRZQ+wew6CHDXncMZR4HM0PEIzUfOEE11Nd6FX5xVQR3x/DlutKf/MuF5ovV7liXZQR6AosCTV2KqgFCz32VMRV8XGrYoCvCD/+Eosg2oJJjssc34STub5u4eZIfLeQH9Tvkly4IZedAv+UBmGIU2EDayN/g67J1uonSR3xf+qwbW/0+6kPyZ2ZeBnH499I8ri6OdlbvV2pQbvShQqBp2yyCJxv8oVn7IyQyW9sM= 18 | on: 19 | tags: true 20 | repo: theGC/html-webpack-inline-svg-plugin 21 | skip_cleanup: 'true' 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.3.0 2 | 3 | * BREAKING CHANGE: On the webpack configuration file the `svgoConfig` option must now go inside `HtmlWebpackInlineSVGPlugin({})` instead of `HtmlWebpackPlugin({})`. An error is thrown at webpack build time otherwise. 4 | * The defaults for the `svgo` module aren't hardcoded anymore and –excepting the `cleanupIDs` option– the defaults are now set by the own module `svgo` and not `html-webpack-inline-svg-plugin`. 5 | 6 | ## v2.2.0 7 | 8 | * Ability added to load SVGs from an URL (``). 9 | 10 | ## v2.0.1 11 | 12 | * added `inlineAll` option to inline all svgs the parser finds 13 | 14 | ## v2.0.0 15 | 16 | * support webpack 4 17 | * support html-webpack-plugin v4 18 | * remove broken html tests as not supported by html-webpack-plugin 19 | * upgrade parse5 to v5 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jan Nicklas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `html-webpack-inline-svg-plugin` 2 | = 3 | 4 | [![npm version](https://badge.fury.io/js/html-webpack-inline-svg-plugin.svg)](https://badge.fury.io/js/html-webpack-inline-svg-plugin) [![Build status](https://travis-ci.org/theGC/html-webpack-inline-svg-plugin.svg)](https://travis-ci.org/theGC/html-webpack-inline-svg-plugin) 5 | 6 | Converts SVG files referenced by `` elements into inlined `` elements within the output HTML of templates used by [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin). 7 | 8 | `html-webpack-inline-svg-plugin` does not support yet webpack v5. 9 | 10 | ## Table of Contents 11 | 12 | 1. [Overview](#overview) 13 | 1. [Installation](#installation) 14 | 1. [Usage](#usage) 15 | 1. [Paths to SVG files](#paths-to-svg-files) 16 | 1. [Relative to OUTPUT](#relative-to-output) 17 | 1. [Relative to ROOT](#relative-to-root) 18 | 1. [Relative to SOURCE](#relative-to-source) 19 | 1. [Incorrect paths or URLs](#incorrect-paths-or-urls) 20 | 1. [Duplicated attributes](#duplicated-attributes) 21 | 1. [`webpack-dev-server`](#webpack-dev-server) 22 | 1. [Configuration](#configuration) 23 | 1. [`runPreEmit`](#runPreEmit) 24 | 1. [`inlineAll`](#inlineAll) 25 | 1. [`allowFromUrl`](#allowFromUrl) 26 | 1. [`svgoConfig`](#svgoConfig) 27 | 1. [Versions](#versions) 28 | 1. [Contributors](#contributors) 29 | 30 | ## Overview 31 | 32 | When SVGs files are inlined into HTML the embedded SVGs can be customised with CSS and the [`fill`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill) rule, and they can also be combined in sprites. Check the css-tricks.com article [Icon System with SVG Sprites](https://css-tricks.com/svg-sprites-use-better-icon-fonts/) for an extended explanation. 33 | 34 | `html-webpack-inline-svg-plugin` implements the below features: 35 | 36 | * **Optimises and minimizes** the inlined SVG with [SVGO](https://github.com/svg/svgo). 37 | * Supports **webpack aliases** for file paths (only when loaders are used). 38 | * Supports the [**`webpack-dev-server`**](https://github.com/webpack/webpack-dev-server). 39 | * Can load image files locally and **from an online URL** with the [`allowFromUrl`](#allowFromUrl) config option. 40 | * Allows for **deep nested SVGs**. 41 | * **Ignores broken tags** (in case you are outputting templates for various parts of the page). 42 | * **Performs no-HTML decoding** (supports language tags, i.e. ``). 43 | 44 | ## Installation 45 | 46 | Add and install the `html-webpack-inline-svg-plugin` dependency to `package.json`. At this point you should already have installed in your project other required dependencies like [webpack](https://www.npmjs.com/package/webpack) and [html-webpack-plugin](https://www.npmjs.com/package/html-webpack-plugin): 47 | 48 | ```bash 49 | npm i -D html-webpack-inline-svg-plugin 50 | ``` 51 | 52 | or with [Yarn](https://yarnpkg.com): 53 | 54 | ```bash 55 | yarn add -D html-webpack-inline-svg-plugin 56 | ``` 57 | 58 | ## Usage 59 | 60 | Given the below reference folder structure: 61 | 62 | ``` 63 | my-project 64 | ├─ package.json 65 | ├─ webpack.config.js 66 | ├─ node_modules 67 | ├─ src 68 | │ ├─ entry.html 69 | │ └─ imagesSource 70 | │ ├─ icon1.svg 71 | │ └─ image1.png 72 | ├─ assets 73 | │ └─ bar.svg 74 | └─ output 75 | ├─ index.html 76 | └─ imagesOutput 77 | ├─ icon2.svg 78 | └─ image2.png 79 | ``` 80 | 81 | On your webpack config file add: 82 | 83 | ```js 84 | // webpack.config.js 85 | 86 | const HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin'); 87 | 88 | // ... 89 | 90 | plugins: [ 91 | new HtmlWebpackPlugin(), 92 | new HtmlWebpackInlineSVGPlugin( 93 | { /* ... */ } // ...Object with config options, if any. Pass nothing otherwise (not even empty object) 94 | ) 95 | ] 96 | ``` 97 | 98 | Then, add `` elements with the `inline` attribute to the templates that html-webpack-plugin will be processing. Check [Paths to SVG files](#paths-to-svg-files) to know what is the correct path to SVG files in `` elements. 99 | 100 | ```html 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | ``` 119 | 120 | ### Paths to SVG files 121 | 122 | There are three ways of referencing SVG files from `` elements in your templates: 123 | 124 | #### Relative to OUTPUT 125 | 126 | If **loaders for the entry template are not used** SVG file paths must be relative to the HTML template that is referencing the SVG files within the webpack [**output**](https://webpack.js.org/configuration/output/#outputpath) folder: 127 | 128 | ```html 129 | 130 | 131 | 132 | 133 | 134 | 135 | ``` 136 | You have to make sure SVG files have been moved to the output directory by some mean. 137 | 138 | In this way SVG files are inlined after all template and image files have been written to the [output directory](https://webpack.js.org/configuration/output/#outputpath), that is it, on html-webpack-plugin's [`afterEmit`](https://github.com/jantimon/html-webpack-plugin#afteremit-hook) event. 139 | 140 | #### Relative to ROOT 141 | 142 | If **loaders for the entry template are still not used**, the **[`runPreEmit`](#runPreEmit) config option** can be used. Then, SVG file paths must be relative to the **project root** (where `package.json` is): 143 | 144 | ```html 145 | 146 | 147 | 148 | 149 | 150 | 151 | ``` 152 | 153 | In this way the plugin run prior to the output of templates, that is it, on html-webpack-plugin's [`beforeEmit`](https://github.com/jantimon/html-webpack-plugin#beforeemit-hook) event. This allows to reference image files from the project root which can help with getting to certain files, like for example within the `node_modules` directory. 154 | 155 | #### Relative to SOURCE 156 | 157 | If **loaders are used** SVG file paths in `` elements must be relative to the **source entry** template (the [`template` config option](https://github.com/jantimon/html-webpack-plugin#options) in html-webpack-plugin). 158 | 159 | The usual loader's combo is the [`html-loader`](https://github.com/webpack-contrib/html-loader) with the [`file-loader`](https://github.com/webpack-contrib/file-loader) (`html-webpack-inline-svg-plugin` does not support yet webpack v5 [asset modules](https://webpack.js.org/guides/asset-modules)). By default html-webpack-plugin uses an [`ejs` loader](https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md) if no loader is provided for the entry template. This default loader does not handle file imports. That is why we need the `html-loader` to parse the entry HTML template, loader that will fire an import event every time it parses a JavaScript, CSS or image import. Then, the `file-loader` will handle SVG image imports, **webpack aliases**, and finally copy the SVG files to the `output` folder. 160 | 161 | Although SVG file paths are relative to the source template the files still need to be copied/emitted to the output folder (which will be done automatically by the `file-loader`): 162 | 163 | ```js 164 | // webpack.config.js 165 | 166 | const path = require('path') 167 | 168 | resolve: { 169 | alias: { 170 | a: path.join(__dirname, 'assets') // With paths relative to SOURCE aliases can be used 171 | } 172 | }, 173 | module: { 174 | rules: [ 175 | { 176 | test: /\.svg$/, 177 | loader: 'file-loader', 178 | options: { 179 | name: 'itCanBeWhatever/[name].[ext]' // It does not have to follow same path or file name than files in 'src' 180 | }, 181 | }, 182 | { 183 | test: /\.html$/, 184 | loader: 'html-loader' 185 | } 186 | ] 187 | }, 188 | ``` 189 | ```html 190 | 191 | 192 | 193 | 194 | 195 | 196 | ``` 197 | 198 | ### Incorrect paths or URLs 199 | 200 | If for any reason the path to a local SVG file is incorrect, or the file fails to be read, or an image retrieved with an URL fails to download, the webpack build process will fail with an `ENOENT` error. 201 | 202 | ### Duplicated attributes 203 | 204 | All the attributes of a `` element excepting `src` and `inline` will be copied to the inlined `` element. Attributes like `id` or `class` will be copied to the resulting root of the `` element and if the original SVG file already had these attributes they will be duplicated (and not replaced) on the resulting `` element, though the attributes coming from the `` will appear first and [any subsequent duplicated attribute from the original SVG will be ignored by the browser](https://stackoverflow.com/questions/26341507/can-an-html-element-have-the-same-attribute-twice). 205 | 206 | For example: 207 | 208 | ```html 209 | 210 | 211 | ``` 212 | ```html 213 | 214 | ... 215 | ``` 216 | 217 | will result in: 218 | 219 | ```html 220 | 221 | ... 222 | ``` 223 | 224 | The broswer will use `id="myImageIMG"` and not `id="myImageSVG"`. It's however a better approach if you avoid having any duplicated attribute at all and only putting the required ones on the `` element. 225 | 226 | ### `webpack-dev-server` 227 | 228 | [Paths relative to SOURCE](#relative-to-source) is the simpler method for [`webpack-dev-server`](https://github.com/webpack/webpack-dev-server) to work with `html-webpack-inline-svg-plugin` because source files, files that are not in the output folder, are the ones referenced. Still, the `file-loader`'s [`emitFile`](https://v4.webpack.js.org/loaders/file-loader/#emitfile) option cannot ever be `false`. 229 | 230 | [Paths relative to OUTPUT](#relative-to-output) or [Paths relative to ROOT](#relative-to-root) can also be used for `webpack-dev-server` as long as they point to SVG files that already exist without the need of a webpack run, that is it, files that are outside the output folder. However using long relative paths to point to such files –since aliases are only available with paths relative to SOURCE– could be a bit tedious. 231 | 232 | 233 | 234 | Check [this issue](https://github.com/theGC/html-webpack-inline-svg-plugin/issues/19) in case you do not get the `webpack-dev-server` working. 235 | 236 | 237 | ## Configuration 238 | 239 | The plugin accepts the below options: 240 | 241 | ### `runPreEmit` 242 | 243 | Defaults to `false`. 244 | 245 | If **loaders** are not used to resolve file locations, and you would prefer to reference SVG file paths relative to the project **root** (where `package.json` is) then set `runPreEmit` config option to `true`: 246 | 247 | ```js 248 | plugins: [ 249 | new HtmlWebpackPlugin(), 250 | new HtmlWebpackInlineSVGPlugin({ 251 | runPreEmit: true, 252 | }) 253 | ] 254 | ``` 255 | 256 | The plugin will now run prior to html-webpack-plugin saving templates to the output directory. Therefore, inlining SVG files would look like: 257 | 258 | ```html 259 | 260 | 261 | 262 | ``` 263 | 264 | ### `inlineAll` 265 | 266 | Defaults to `false`. 267 | 268 | It will inline all SVG images on the template without the need of the `inline` attribute on every image: 269 | 270 | ```js 271 | plugins: [ 272 | new HtmlWebpackPlugin(), 273 | new HtmlWebpackInlineSVGPlugin({ 274 | inlineAll: true 275 | }) 276 | ] 277 | ``` 278 | 279 | If `inlineAll` option is enabled you can use the `inline-exclude` attribute to exclude a particular image from being inlined: 280 | 281 | ```html 282 | 283 | 284 |
285 | 286 | 287 |
288 | ``` 289 | 290 | ### `allowFromUrl` 291 | 292 | Defaults to `false`. 293 | 294 | It allows to use SVG images coming from an URL in addition to local files: 295 | 296 | ```js 297 | plugins: [ 298 | new HtmlWebpackPlugin(), 299 | new HtmlWebpackInlineSVGPlugin({ 300 | allowFromUrl: true 301 | }) 302 | ] 303 | ``` 304 | 305 | For example: 306 | 307 | ```html 308 | 309 | 310 |
311 | 312 |
313 | ``` 314 | 315 | ### `svgoConfig` 316 | 317 | Defaults to `[]`. 318 | 319 | [SVGO](https://github.com/svg/svgo) is used to optimise the SVGs inlined. You can configure SVGO by setting this `svgoConfig` array with the [SVGO plugins](https://github.com/svg/svgo#what-it-can-do) you need in the same way it's done in this [SVGO official Node.js example](https://github.com/svg/svgo/blob/master/examples/test.js). 320 | 321 | Note `svgoConfig` is an array of `Object`s that will be assigned to the `.plugins` SVGO config variable by `html-webpack-inline-svg-plugin`. You don't need to pass an `Object` with a `plugins` property assigned an array of SVGO plugins, just pass the array: 322 | 323 | ```js 324 | plugins: [ 325 | new HtmlWebpackPlugin(), 326 | new HtmlWebpackInlineSVGPlugin({ 327 | svgoConfig: [ 328 | { 329 | removeViewBox: false 330 | }, 331 | { 332 | inlineStyles: { 333 | onlyMatchedOnce: false 334 | } 335 | } 336 | ] 337 | }) 338 | ] 339 | ``` 340 | 341 | `html-webpack-inline-svg-plugin` modifies one SVGO default: `cleanupIDs`, from `true` to `false`, since IDs allow to reference individual symbols. You can still override this or any other SVGO plugin default configuration with this `svgoConfig` option. 342 | 343 | ## Versions 344 | 345 | The latest version of this package supports webpack 4. All versions marked v2.x.x will target webpack 4 and html-webpack-plugin v4. 346 | 347 | For webpack 3 and html-webpack-plugin v3 support use v1.3.0 of this package. 348 | 349 | ### v2.x.x 350 | - Support webpack v4. 351 | - Support html-webpack-plugin v4. 352 | 353 | ### v1.3.0 354 | - Support webpack v3. 355 | - Support html-webpack-plugin v3. 356 | 357 | 358 | ## Contributors 359 | 360 | You're free to contribute to this project by submitting issues and/or pull requests. This project is test-driven, so keep in mind that every change and new feature must be covered by tests. 361 | 362 | I'm happy for someone to take over the project as I don't find myself using it any longer due to changes in workflow. Therefore others are likely to be in a better position to support this project and roll out the right enhancements. 363 | 364 | 365 | 366 | 367 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const chalk = require('chalk') 5 | const parse5 = require('parse5') 6 | const _ = require('lodash') 7 | const fs = require('fs') 8 | const SVGO = require('svgo') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const axios = require('axios') 11 | 12 | 13 | /** 14 | * class to inline SVGs within html-webpack-plugin templates 15 | * 16 | */ 17 | class HtmlWebpackInlineSVGPlugin { 18 | 19 | constructor (options) { 20 | 21 | this.runPreEmit = _.get(options, 'runPreEmit', false) 22 | this.inlineAll = _.get(options, 'inlineAll', false) 23 | this.allowFromUrl = _.get(options, 'allowFromUrl', false) 24 | this.svgoConfig = _.get(options, 'svgoConfig', []) 25 | this.outputPath = '' 26 | 27 | this.files = [] 28 | 29 | } 30 | 31 | 32 | /** 33 | * required to create a webpack plugin 34 | * @param {object} compiler - webpack compiler 35 | * 36 | */ 37 | apply (compiler) { 38 | 39 | 40 | // Hook into the html-webpack-plugin processing 41 | 42 | compiler.hooks.compilation.tap('HtmlWebpackInlineSVGPlugin', (compilation) => { 43 | 44 | if (this.runPreEmit) { 45 | 46 | HtmlWebpackPlugin 47 | .getHooks(compilation) 48 | .afterTemplateExecution 49 | .tapAsync('HtmlWebpackInlineSVGPlugin', (htmlPluginData, callback) => { 50 | 51 | // get the custom config 52 | 53 | this.getUserConfig(htmlPluginData) 54 | 55 | 56 | // process the images 57 | 58 | return this.processImages(htmlPluginData.html) 59 | .then((html) => { 60 | 61 | htmlPluginData.html = html || htmlPluginData.html 62 | 63 | return typeof callback === 'function' ? 64 | callback(null, htmlPluginData) : 65 | htmlPluginData 66 | 67 | }) 68 | .catch((err) => { 69 | 70 | console.log(chalk.red(err)) 71 | 72 | return typeof callback === 'function' ? 73 | callback(null, htmlPluginData) : 74 | htmlPluginData 75 | 76 | }) 77 | 78 | }) 79 | 80 | } else { 81 | // https://github.com/jantimon/html-webpack-plugin/issues/1091 82 | compiler.options.plugins.find((plugin) => plugin.constructor.name === 'HtmlWebpackPlugin').constructor 83 | .getHooks(compilation) 84 | .beforeEmit 85 | .tapAsync('HtmlWebpackInlineSVGPlugin', (htmlPluginData, callback) => { 86 | 87 | // fetch the output path from webpack 88 | 89 | this.outputPath = compilation.outputOptions && 90 | compilation.outputOptions.path ? 91 | compilation.outputOptions.path : 92 | '' 93 | 94 | if (!this.outputPath) { 95 | 96 | console.log(chalk.red('no output path found on compilation.outputOptions')) 97 | 98 | return typeof callback === 'function' ? 99 | callback(null, htmlPluginData) : 100 | htmlPluginData 101 | 102 | } 103 | 104 | 105 | // get the custom config 106 | 107 | this.getUserConfig(htmlPluginData) 108 | 109 | 110 | // get the filename 111 | 112 | const filename = htmlPluginData.outputName ? htmlPluginData.outputName : '' 113 | 114 | if (!filename) { 115 | 116 | console.log(chalk.red('no filename found on htmlPluginData.outputName')) 117 | 118 | return typeof callback === 'function' ? 119 | callback(null, htmlPluginData) : 120 | htmlPluginData 121 | 122 | } 123 | 124 | 125 | // get the emitted HTML - prior to SVG's being inlined 126 | 127 | const originalHtml = htmlPluginData.html 128 | 129 | 130 | // add filename and original html to the file array 131 | 132 | this.files.push({ 133 | filename, 134 | originalHtml, 135 | }) 136 | 137 | 138 | // fire callback to pass control to any further plugins 139 | 140 | return typeof callback === 'function' ? 141 | callback(null, htmlPluginData) : 142 | htmlPluginData 143 | 144 | }) 145 | 146 | } 147 | 148 | }) 149 | 150 | 151 | // hook after-emit so we can read the generated SVG assets within 152 | // the output directory 153 | 154 | if (!this.runPreEmit) { 155 | 156 | compiler.hooks.afterEmit.tap('HtmlWebpackInlineSVGPlugin', (compilation) => { 157 | 158 | if (!this.files.length) { 159 | 160 | console.log(chalk.green('no files passed for SVG inline to process')) 161 | 162 | return 163 | 164 | } 165 | 166 | 167 | // iterate over each file and inline it's SVGs 168 | // then return a callback if available 169 | 170 | return Promise.all(this.files.map(file => this.processImages(file.originalHtml))) 171 | .then((htmlArray) => Promise.all(htmlArray.map((html, index) => this.updateOutputFile(html, this.files[index].filename)))) 172 | .then(() => typeof callback === 'function' ? callback() : null) 173 | .catch((err) => console.log(chalk.red(err))) 174 | 175 | }) 176 | 177 | } 178 | 179 | } 180 | 181 | 182 | /** 183 | * DEPRECATED: svgoConfig option must now go inside HtmlWebpackInlineSVGPlugin({}) instead of HtmlWebpackPlugin({}). 184 | * This method will be removed in future releases. 185 | * 186 | * get the users custom config 187 | * @param {Object} htmlPluginData 188 | * 189 | */ 190 | getUserConfig (htmlPluginData) { 191 | 192 | if (_.get(htmlPluginData, 'plugin.options.svgoConfig', false)) { 193 | throw new Error('html-webpack-inline-svg-plugin: on your webpack configuration file svgoConfig option must now go inside HtmlWebpackInlineSVGPlugin({}) instead of HtmlWebpackPlugin({}). Also note the SVGO configuration format has changed and the one you had will need tweaking: https://github.com/theGC/html-webpack-inline-svg-plugin#config') 194 | } 195 | 196 | } 197 | 198 | 199 | /** 200 | * once we've inlined all SVGs and generated the final html 201 | * we need to write it to the file output by html-webpack-plugin 202 | * Note: we can not simply update the callbacks html as we are 203 | * working with the emitted data due to allowing for webpack to first 204 | * resolve and output all files 205 | * @param {string} html - processed and updated html with inlined SVGs 206 | * @param {string} filename - the template file we are currently processing 207 | * @returns {Promise} 208 | * 209 | */ 210 | updateOutputFile (html, filename) { 211 | 212 | if (!this.outputPath || !filename) return Promise.reject(new Error('outputPath & filename must be set to update output file')) 213 | 214 | else if (!html) return Promise.resolve() 215 | 216 | return new Promise((resolve, reject) => { 217 | 218 | 219 | // set the output file to the updated html 220 | 221 | fs.writeFile(path.resolve(this.outputPath, filename), html, (err) => { 222 | 223 | if (err) { 224 | 225 | reject(err) 226 | 227 | return 228 | 229 | } 230 | 231 | resolve() 232 | 233 | }) 234 | 235 | }) 236 | 237 | } 238 | 239 | 240 | /** 241 | * find all inline images and replace their html within the output 242 | * @param {string} html - generated html from html-webpack-plugin 243 | * @returns {Promise} 244 | * 245 | */ 246 | processImages (html) { 247 | 248 | return new Promise((resolve, reject) => { 249 | 250 | const documentFragment = parse5.parseFragment(html, { 251 | sourceCodeLocationInfo: true 252 | }) 253 | 254 | 255 | // grab the images to process from the original DOM fragment 256 | 257 | const inlineImages = this.getInlineImages(documentFragment) 258 | 259 | 260 | // if we have no inlined images return the html 261 | 262 | if (!inlineImages.length) return resolve() 263 | 264 | 265 | // process the imageNodes 266 | 267 | this.updateHTML(html, inlineImages) 268 | .then((html) => resolve(html)) 269 | .catch((err) => { 270 | 271 | console.log(chalk.underline.red('processImages hit error')) 272 | console.log(chalk.red(err)) 273 | 274 | reject(err) 275 | 276 | }) 277 | 278 | }) 279 | 280 | } 281 | 282 | 283 | /** 284 | * run the Promises in a synchronous order 285 | * allows us to ensure we have completed processing of an inline image 286 | * before the next ones Promise is called (via then chaining) 287 | * @param {object} html 288 | * @param {array} inlineImages 289 | * @returns {Promise} 290 | * 291 | */ 292 | updateHTML (html, inlineImages) { 293 | 294 | return inlineImages.reduce((promise, imageNode) => { 295 | 296 | return promise 297 | .then((html) => { 298 | 299 | return this.processImage(html) 300 | 301 | }) 302 | .catch(err => console.log(err)) 303 | 304 | }, Promise.resolve(html)) 305 | 306 | } 307 | 308 | 309 | /** 310 | * get the first inline image and replace it with its inline SVG 311 | * @returns {Promise} 312 | * 313 | */ 314 | processImage (html) { 315 | 316 | return new Promise((resolve, reject) => { 317 | 318 | 319 | // rebuild the document fragment each time with the updated html 320 | 321 | const documentFragment = parse5.parseFragment(html, { 322 | sourceCodeLocationInfo: true, 323 | }) 324 | 325 | const inlineImage = this.getFirstInlineImage(documentFragment) 326 | 327 | if (inlineImage) { 328 | 329 | this.processOutputHtml(html, inlineImage) 330 | .then((html) => { 331 | 332 | resolve(html) 333 | 334 | }) 335 | .catch((err) => reject(err)) 336 | 337 | } else { 338 | 339 | 340 | // no inline image - just resolve 341 | 342 | resolve(html) 343 | 344 | } 345 | 346 | }) 347 | 348 | } 349 | 350 | 351 | /** 352 | * get a count for how many inline images the html document contains 353 | * @param {Object} documentFragment - parse5 processed html 354 | * @param {array} inlineImages 355 | * @returns {array} 356 | * 357 | */ 358 | getInlineImages (documentFragment, inlineImages) { 359 | 360 | if (!inlineImages) inlineImages = [] 361 | 362 | if (documentFragment.childNodes && documentFragment.childNodes.length) { 363 | 364 | documentFragment.childNodes.forEach((childNode) => { 365 | 366 | if (this.isNodeValidInlineImage(childNode)) { 367 | 368 | inlineImages.push(childNode) 369 | 370 | } else { 371 | 372 | inlineImages = this.getInlineImages(childNode, inlineImages) 373 | 374 | } 375 | 376 | }) 377 | 378 | } 379 | 380 | return inlineImages 381 | 382 | } 383 | 384 | 385 | /** 386 | * return the first inline image or false if none 387 | * @param {Object} documentFragment - parse5 processed html 388 | * @returns {null|Object} - null if no inline image - parse5 documentFragment if there is 389 | * 390 | */ 391 | getFirstInlineImage (documentFragment) { 392 | 393 | const inlineImages = this.getInlineImages(documentFragment) 394 | 395 | if (!inlineImages.length) return null 396 | 397 | return inlineImages[0] 398 | 399 | } 400 | 401 | 402 | /** 403 | * check if a node is a valid inline image 404 | * @param {Object} node - parse5 documentFragment 405 | * @returns {boolean} 406 | * 407 | */ 408 | isNodeValidInlineImage (node) { 409 | 410 | return !!( 411 | node.nodeName === 'img' 412 | && ((this.inlineAll && _.filter(node.attrs, { name: 'inline-exclude' }).length === 0) 413 | || _.filter(node.attrs, { name: 'inline' }).length) 414 | && this.getImagesSrc(node)) 415 | 416 | } 417 | 418 | 419 | /** 420 | * get an inlined images src 421 | * @param {Object} inlineImage - parse5 document 422 | * @returns {string} 423 | * 424 | */ 425 | getImagesSrc (inlineImage) { 426 | 427 | const svgSrcObject = _.find(inlineImage.attrs, { name: 'src' }) 428 | 429 | // image does not have a src attribute 430 | 431 | if (!svgSrcObject) return '' 432 | 433 | 434 | // grab the image src 435 | 436 | const svgSrc = svgSrcObject.value 437 | 438 | 439 | // image src attribute must not be blank and it must be referencing a file with a .svg extension 440 | 441 | return svgSrc && svgSrc.indexOf('.svg') !== -1 ? svgSrc : '' 442 | 443 | } 444 | 445 | /** 446 | * append the inlineImages SVG data to the output HTML and remove the original img 447 | * @param {Object{}} html 448 | * @param {Object{}} inlineImage - parse5 document 449 | * @param {Object{}} data - SVG data 450 | * @param {Object{}} resolve - Consumer's Promise resolve hook 451 | * @returns {Promise} 452 | * 453 | */ 454 | optimizeSvg ({ html, inlineImage, data, resolve }) { 455 | const svgo = new SVGO({ 456 | plugins: this.getSvgoConfig() 457 | }) 458 | 459 | svgo.optimize(data) 460 | .then((result) => { 461 | 462 | const optimisedSVG = result.data 463 | 464 | html = this.replaceImageWithSVG(html, inlineImage, optimisedSVG) 465 | 466 | resolve(html) 467 | 468 | }) 469 | .catch((err) => console.log(chalk.red(err))) 470 | } 471 | 472 | /** 473 | * Returns an array with he default SVGO configuration merged with the configuration provided by the user. 474 | * The configuration provided by the user overrides the default one. 475 | * @returns {Array} 476 | * 477 | */ 478 | getSvgoConfig() { 479 | const svgoDefaultConfig = [ 480 | { cleanupIDs: false } 481 | ] 482 | 483 | const svgoDefaultConfigFiltered = svgoDefaultConfig.filter(di => 484 | !this.svgoConfig.some(i => Object.keys(di)[0] === Object.keys(i)[0]) 485 | ) 486 | 487 | return svgoDefaultConfigFiltered.concat(this.svgoConfig) 488 | } 489 | 490 | /** 491 | * append the inlineImages SVG data to the output HTML and remove the original img by 492 | * loading the SVG data from the filesystem or from an URL 493 | * @param {string} html 494 | * @param {Object} inlineImage - parse5 document 495 | * @returns {Promise} 496 | * 497 | */ 498 | processOutputHtml (html, inlineImage) { 499 | 500 | return new Promise((resolve, reject) => { 501 | 502 | const svgSrc = this.getImagesSrc(inlineImage) 503 | 504 | 505 | // if the image isn't valid resolve 506 | if (!svgSrc) return resolve(html) 507 | 508 | // read in the svg 509 | fs.readFile(path.resolve(this.outputPath, svgSrc), 'utf8', (err, data) => { 510 | if (!err) { 511 | this.optimizeSvg({ html, inlineImage, data, resolve }) 512 | return 513 | } 514 | 515 | // loading from the filesystem failed 516 | if (!this.allowFromUrl) { 517 | reject(err) 518 | return 519 | } 520 | 521 | axios.get(svgSrc) 522 | .then(({ data, status }) => { 523 | if (status !== 200) { 524 | throw new Error(`Error when retrieving image from URL: ${status} status`) 525 | } 526 | this.optimizeSvg({ html, inlineImage, svgSrc, data, resolve }) 527 | }) 528 | .catch((err) => reject(err)) 529 | 530 | 531 | }) 532 | 533 | }) 534 | 535 | } 536 | 537 | 538 | /** 539 | * replace the img with the optimized SVG 540 | * @param {string} html 541 | * @param {Object} inlineImage - parse5 document 542 | * @param {Object} svg 543 | * 544 | */ 545 | replaceImageWithSVG(html, inlineImage, svg) { 546 | 547 | // Get all passed image attributes except 'inline' and 'src' 548 | const imgAttributes = inlineImage.attrs.reduce((acc, attr) => { 549 | 550 | const { name, value } = attr 551 | 552 | return name !== 'inline' 553 | && name !== 'src' 554 | ? acc + `${name}="${value}" ` 555 | : acc 556 | 557 | }, '') 558 | 559 | 560 | // Set the attributes to the output svg 561 | if (imgAttributes) { 562 | 563 | svg = svg.replace(' { 8 | 9 | if (err) console.log(chalk.red(err)) 10 | 11 | }) 12 | 13 | 14 | // add pre or post emit config to base 15 | 16 | // Object.assign(webpackConfig.options, webpackPostEmitConfig) 17 | Object.assign(webpackConfig.options, webpackPreEmitConfig) 18 | 19 | 20 | /** 21 | * no testing - just attempt to output the dist folder and files 22 | * 23 | */ 24 | 25 | webpack(webpackConfig.options, function (err) { 26 | 27 | if (err) { 28 | 29 | console.log(chalk.red(err)) 30 | 31 | return 32 | 33 | } 34 | 35 | console.log(chalk.green('build complete')) 36 | 37 | }) 38 | -------------------------------------------------------------------------------- /spec/fixtures/blank.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Inline SVG Tests blank 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

should not touch this:

17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /spec/fixtures/entry.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theGC/html-webpack-inline-svg-plugin/55b17ac5d70b66f84d9f6cce2ed583a61c50def9/spec/fixtures/entry.js -------------------------------------------------------------------------------- /spec/fixtures/images/another-face.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/images/face.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/images/moods.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | icon mood-bad 7 | 8 | 9 | 10 | icon mood good 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/images/not-an-svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theGC/html-webpack-inline-svg-plugin/55b17ac5d70b66f84d9f6cce2ed583a61c50def9/spec/fixtures/images/not-an-svg.png -------------------------------------------------------------------------------- /spec/fixtures/index-allow-from-url.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Allow From URL SVG Tests index 8 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |

should not touch this:

26 |

27 | if below SVG is inlined correctly we can reference its content: 28 | 29 | 30 | 31 | 32 |

33 |
34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 |
bar; ?>
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /spec/fixtures/index-inline-all.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Inline SVG Tests index 8 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |

27 | if below SVG is inlined correctly we can reference its content: 28 | 29 |

30 |
31 | 32 |
33 |

Deep Replace

34 |
35 | 36 | 37 | 38 |
39 |
40 | 41 |

Inline images

42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
bar; ?>
50 | 51 | should output broken tags

52 | 53 |

should output unclosed tags 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /spec/fixtures/index-pre-emit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Inline SVG Tests index 8 | 20 | 21 | 22 | 23 | 24 | 25 |

26 |

should not touch this:

27 |

28 | if below SVG is inlined correctly we can reference its content: 29 | 30 |

31 |
32 | 33 |
34 |

Deep Replace

35 |
36 | 37 | 38 | 39 |
40 |
41 | 42 |

Inline images

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
bar; ?>
51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /spec/fixtures/index-svgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Inline SVG Tests index 8 | 9 | 10 | 11 |
12 |

13 | if below SVG is inlined correctly we can reference its content: 14 | 15 |

16 |
17 | 18 |

Inline images

19 | 20 | 21 | 22 | 23 | 24 |
bar; ?>
25 | 26 | should output broken tags

27 | 28 |

should output unclosed tags 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /spec/fixtures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Inline SVG Tests index 8 | 20 | 21 | 22 | 23 | 24 | 25 |

26 |

should not touch this:

27 |

28 | if below SVG is inlined correctly we can reference its content: 29 | 30 |

31 |
32 | 33 |
34 |

Deep Replace

35 |
36 | 37 | 38 | 39 |
40 |
41 | 42 |

Inline images

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
bar; ?>
51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /spec/fixtures/partial-pre-emit.html: -------------------------------------------------------------------------------- 1 |
2 |

should not touch this:

3 |

4 | if below SVG is inlined correctly we can reference its content: 5 | 6 | 7 |

8 |
9 | 10 | 11 | 12 | 13 | 14 |
bar; ?>
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/fixtures/partial.html: -------------------------------------------------------------------------------- 1 |
2 |

should not touch this:

3 |

4 | if below SVG is inlined correctly we can reference its content: 5 | 6 | 7 |

8 |
9 | 10 | 11 | 12 | 13 | 14 |
bar; ?>
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | var chalk = require('chalk') 3 | var webpack = require('webpack') 4 | var axios = require('axios') 5 | var MockAdapter = require('axios-mock-adapter') 6 | var webpackConfig = require('./webpack.base.config') 7 | var webpackPostEmitConfig = require('./webpack.post-emit.config') 8 | var webpackPreEmitConfig = require('./webpack.pre-emit.config') 9 | var webpackInlineAllConfig = require('./webpack.inline-all.config') 10 | var webpackAllowFromUrlConfig = require('./webpack.allow-from-url.config') 11 | var webpackSvgoConfig = require('./webpack.svgo.config') 12 | var jasmineTests = require('./jasmine.tests') 13 | var jasmineInlineAllTests = require('./jasmine-inline-all.tests') 14 | var jasmineAllowFromUrlTests = require('./jasmine-allow-from-url.tests') 15 | var jasmineSvgoTests = require('./jasmine-svgo.tests') 16 | var rm = require('rimraf') 17 | 18 | rm(webpackConfig.outputDir, (err) => { 19 | 20 | if (err) console.log(chalk.red(err)) 21 | 22 | }) 23 | 24 | describe('HtmlWebpackInlineSVGPlugin: post webpack resolve', function () { 25 | 26 | beforeAll(function (done) { 27 | 28 | // clone the config 29 | 30 | const webpackTestConfig = Object.assign({}, webpackConfig.options, webpackPostEmitConfig) 31 | 32 | 33 | // run webpack 34 | 35 | webpack(webpackTestConfig, (err) => { 36 | 37 | expect(err).toBeFalsy() 38 | 39 | // callback is fired before all files have been written to disk 40 | // due to use of after-emit - place a timeout to try and avoid the issue 41 | 42 | setTimeout(done, 2000) 43 | 44 | }) 45 | 46 | }) 47 | 48 | 49 | // run all tests 50 | 51 | jasmineTests.forEach((test) => { 52 | 53 | it(test.label, test.func) 54 | 55 | }) 56 | 57 | }) 58 | 59 | describe('HtmlWebpackInlineSVGPlugin: allowFromUrl webpack resolve', function () { 60 | 61 | var mock = new MockAdapter(axios) 62 | 63 | beforeAll(function (done) { 64 | mock.onGet('https://badge.fury.io/js/html-webpack-inline-svg-plugin.svg').reply(200, 'mocked svg') 65 | 66 | // Like when the SVG file is retrieved locally, the webpack build process fails as well if the URL provided doesn't download for some reason 67 | // mock.onGet('https://notFound/typoInExtension/html-webpack-inline-svg-plugin-typoInNaming.svg').reply(404) 68 | // mock.onGet('http://errorLoading/someIconWhichDoesNotExist.svg').reply(500) 69 | // mock.onGet('http://timeoutLoading/someIconWhichDoesNotExist-timeout.svg').timeout(); 70 | 71 | // clone the config 72 | const webpackTestConfig = Object.assign({}, webpackConfig.options, webpackAllowFromUrlConfig) 73 | 74 | 75 | // run webpack 76 | 77 | webpack(webpackTestConfig, (err) => { 78 | 79 | expect(err).toBeFalsy() 80 | 81 | // callback is fired before all files have been written to disk 82 | // due to use of after-emit - place a timeout to try and avoid the issue 83 | 84 | setTimeout(done, 2000) 85 | 86 | }) 87 | 88 | }) 89 | 90 | afterEach(() => { 91 | mock.restore(); 92 | }) 93 | 94 | 95 | // run all-images-from-url tests 96 | 97 | jasmineAllowFromUrlTests.forEach((test) => { 98 | it(test.label, test.func) 99 | }) 100 | 101 | }) 102 | 103 | 104 | // second set of tests running prior to webpack resolves 105 | 106 | describe('HtmlWebpackInlineSVGPlugin: pre webpack resolve', function () { 107 | 108 | beforeAll(function (done) { 109 | 110 | // clone the config 111 | 112 | const webpackTestConfig = Object.assign({}, webpackConfig.options, webpackPreEmitConfig) 113 | 114 | 115 | // run webpack 116 | 117 | webpack(webpackTestConfig, (err) => { 118 | 119 | expect(err).toBeFalsy() 120 | 121 | // callback is fired before all files have been written to disk 122 | // due to use of after-emit - place a timeout to try and avoid the issue 123 | 124 | setTimeout(done, 2000) 125 | 126 | }) 127 | 128 | }) 129 | 130 | 131 | // run all tests 132 | 133 | jasmineTests.forEach((test) => { 134 | 135 | it(test.label, test.func) 136 | 137 | }) 138 | 139 | }) 140 | 141 | 142 | describe('HtmlWebpackInlineSVGPlugin: inlineAll resolve', function () { 143 | 144 | beforeAll(function (done) { 145 | 146 | // clone the config 147 | 148 | const webpackTestConfig = Object.assign({}, webpackConfig.options, webpackInlineAllConfig) 149 | 150 | 151 | // run webpack 152 | 153 | webpack(webpackTestConfig, (err) => { 154 | 155 | expect(err).toBeFalsy() 156 | 157 | // callback is fired before all files hve been written to disk 158 | // due to use of after-emit - place a timeout to try and avoid the issue 159 | 160 | setTimeout(done, 2000) 161 | 162 | }) 163 | 164 | }) 165 | 166 | 167 | // run all-images tests 168 | 169 | jasmineInlineAllTests.forEach((test) => { 170 | 171 | it(test.label, test.func) 172 | 173 | }) 174 | 175 | }) 176 | 177 | 178 | describe('HtmlWebpackInlineSVGPlugin: custom SVGO config resolve.', function () { 179 | 180 | beforeAll(function (done) { 181 | 182 | // clone the config 183 | 184 | const webpackTestConfig = Object.assign({}, webpackConfig.options, webpackSvgoConfig) 185 | 186 | 187 | // run webpack 188 | 189 | webpack(webpackTestConfig, (err) => { 190 | 191 | expect(err).toBeFalsy() 192 | 193 | // callback is fired before all files hve been written to disk 194 | // due to use of after-emit - place a timeout to try and avoid the issue 195 | 196 | setTimeout(done, 2000) 197 | 198 | }) 199 | 200 | }) 201 | 202 | jasmineSvgoTests.forEach((test) => { 203 | 204 | it(test.label, test.func) 205 | 206 | }) 207 | 208 | }) 209 | -------------------------------------------------------------------------------- /spec/jasmine-allow-from-url.tests.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var fs = require('fs') 3 | var cheerio = require('cheerio') 4 | var webpackConfig = require('./webpack.base.config') 5 | 6 | module.exports = [ 7 | 8 | { 9 | label: 'should inline all SVG images (even the ones being an URL) and exclude images with the inline-exclude attribute', 10 | func: function (done) { 11 | 12 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index-allow-from-url.html') 13 | 14 | return fs.readFile(htmlFile, 'utf8', function (err, data) { 15 | expect(err).toBeFalsy() 16 | 17 | var $ = cheerio.load(data) 18 | 19 | expect($('img#find-and-replace-me').length).toBe(1) 20 | 21 | expect($('svg#find-and-replace-me').length).toBe(0) 22 | 23 | expect($('img#replace-me').length).toBe(0) 24 | 25 | expect($('svg#replace-me').length).toBe(1) 26 | 27 | expect($('img#replace-me-too').length).toBe(0) 28 | 29 | expect($('svg#replace-me-too').length).toBe(1) 30 | 31 | expect($('img#not-an-svg').length).toBe(1) 32 | 33 | expect($('div#do-not-decode').length).toBe(1) 34 | 35 | done() 36 | 37 | }) 38 | 39 | }, 40 | 41 | }, 42 | 43 | ] 44 | -------------------------------------------------------------------------------- /spec/jasmine-inline-all.tests.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var fs = require('fs') 3 | var cheerio = require('cheerio') 4 | var webpackConfig = require('./webpack.base.config') 5 | 6 | module.exports = [ 7 | 8 | { 9 | label: 'should inline all SVG images and exclude images with the inline-exclude attribute', 10 | func: function (done) { 11 | 12 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index-inline-all.html') 13 | 14 | fs.readFile(htmlFile, 'utf8', function (err, data) { 15 | expect(err).toBeFalsy() 16 | 17 | var $ = cheerio.load(data) 18 | 19 | expect($('img#deep-replace-me').length).toBe(0) 20 | 21 | expect($('img#replace-me').length).toBe(0) 22 | 23 | expect($('svg#deep-replace-me').length).toBe(1) 24 | 25 | expect($('svg#replace-me').length).toBe(1) 26 | 27 | expect($('img#exclude-replace-me').length).toBe(1) 28 | 29 | expect($('img#not-an-svg').length).toBe(1) 30 | 31 | done() 32 | 33 | }) 34 | 35 | }, 36 | 37 | }, 38 | 39 | ] 40 | -------------------------------------------------------------------------------- /spec/jasmine-svgo.tests.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var fs = require('fs') 3 | var cheerio = require('cheerio') 4 | var webpackConfig = require('./webpack.base.config') 5 | 6 | module.exports = [ 7 | 8 | { 9 | label: 'It should inline SVG images following the custom SVGO config', 10 | func: function (done) { 11 | 12 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index-svgo.html') 13 | 14 | fs.readFile(htmlFile, 'utf8', function (err, data) { 15 | expect(err).toBeFalsy() 16 | 17 | var $ = cheerio.load(data) 18 | 19 | // Assertions affecting any html-webpack-inline-svg-plugin use 20 | expect($('img#replace-me').length).toBe(0) 21 | expect($('svg#replace-me').length).toBe(1) 22 | expect($('img#not-an-svg').length).toBe(1) 23 | expect($('div#do-not-decode').length).toBe(1) 24 | 25 | // Assertions affecting only svgoConfig option 26 | // The configuration found on webpack.svgo.config.js is the opposite to SVGO defaults 27 | expect($('svg#replace-me').attr('xmlns')).toBe(undefined) 28 | expect($('svg#replace-me title#icon-mood-bad-title').length).toBe(1) 29 | expect($('svg#replace-me symbol#icon-mood-bad').contents()[0].data).toBe('Random comment') 30 | 31 | done() 32 | 33 | }) 34 | 35 | }, 36 | 37 | }, 38 | 39 | ] 40 | -------------------------------------------------------------------------------- /spec/jasmine.tests.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var fs = require('fs') 3 | var cheerio = require('cheerio') 4 | var webpackConfig = require('./webpack.base.config') 5 | 6 | module.exports = [ 7 | 8 | { 9 | label: 'should not inline imgs without inline attribute', 10 | func: function (done) { 11 | 12 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index.html') 13 | 14 | fs.readFile(htmlFile, 'utf8', function (err, data) { 15 | 16 | expect(err).toBeFalsy() 17 | 18 | var $ = cheerio.load(data) 19 | 20 | expect($('img.leave-me').length).toBe(1) 21 | 22 | done() 23 | 24 | }) 25 | 26 | }, 27 | 28 | }, 29 | 30 | { 31 | label: 'should inline imgs with inline attribute', 32 | func: function (done) { 33 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index.html') 34 | 35 | fs.readFile(htmlFile, 'utf8', function (err, data) { 36 | 37 | expect(err).toBeFalsy() 38 | 39 | var $ = cheerio.load(data) 40 | 41 | expect($('svg#replace-me').length).toBe(1) 42 | 43 | done() 44 | 45 | }) 46 | 47 | }, 48 | 49 | }, 50 | 51 | { 52 | label: 'should remove img tags with inline attribute', 53 | func: function (done) { 54 | 55 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index.html') 56 | 57 | fs.readFile(htmlFile, 'utf8', function (err, data) { 58 | 59 | expect(err).toBeFalsy() 60 | 61 | var $ = cheerio.load(data) 62 | 63 | expect($('img#replace-me').length).toBe(0) 64 | 65 | done() 66 | 67 | }) 68 | 69 | }, 70 | 71 | }, 72 | 73 | { 74 | label: 'should remove multiple inlined img tags within the same document', 75 | func: function (done) { 76 | 77 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index.html') 78 | 79 | fs.readFile(htmlFile, 'utf8', function (err, data) { 80 | 81 | expect(err).toBeFalsy() 82 | 83 | var $ = cheerio.load(data) 84 | 85 | expect($('img#then-replace-me').length).toBe(0) 86 | 87 | done() 88 | 89 | }) 90 | 91 | }, 92 | 93 | }, 94 | 95 | { 96 | label: 'should ignore images that are not svg', 97 | func: function (done) { 98 | 99 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index.html') 100 | 101 | fs.readFile(htmlFile, 'utf8', function (err, data) { 102 | 103 | expect(err).toBeFalsy() 104 | 105 | var $ = cheerio.load(data) 106 | 107 | expect($('#not-an-svg').length).toBe(1) 108 | 109 | done() 110 | 111 | }) 112 | 113 | }, 114 | 115 | }, 116 | 117 | { 118 | label: 'do not html decode content', 119 | func: function (done) { 120 | 121 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index.html') 122 | 123 | fs.readFile(htmlFile, 'utf8', function (err, data) { 124 | 125 | expect(err).toBeFalsy() 126 | 127 | expect(data.indexOf('bar; ?>')) 128 | .not.toBe(-1) 129 | 130 | done() 131 | 132 | }) 133 | 134 | }, 135 | 136 | }, 137 | 138 | { 139 | label: 'should replace nested inline imgs', 140 | func: function (done) { 141 | 142 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index.html') 143 | 144 | fs.readFile(htmlFile, 'utf8', function (err, data) { 145 | 146 | expect(err).toBeFalsy() 147 | 148 | var $ = cheerio.load(data) 149 | 150 | expect($('svg#deep-replace-me').length).toBe(1) 151 | 152 | done() 153 | 154 | }) 155 | 156 | }, 157 | 158 | }, 159 | 160 | { 161 | label: 'should contain deep inline SVG', 162 | func: function (done) { 163 | 164 | var htmlFile = path.resolve(webpackConfig.outputDir, 'index.html') 165 | 166 | fs.readFile(htmlFile, 'utf8', function (err, data) { 167 | 168 | expect(err).toBeFalsy() 169 | 170 | var $ = cheerio.load(data) 171 | 172 | expect($('svg#deep-replace-me').length).toBe(1) 173 | 174 | done() 175 | 176 | }) 177 | 178 | }, 179 | 180 | }, 181 | 182 | ] 183 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "stopSpecOnExpectationFailure": false, 7 | "random": true 8 | } 9 | -------------------------------------------------------------------------------- /spec/webpack.allow-from-url.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpackConfig = require('./webpack.base.config') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const HtmlWebpackInlineSVGPlugin = require('..') 5 | 6 | module.exports = { 7 | 8 | resolve: { 9 | alias: { 10 | 'img': path.join(__dirname, 'fixtures', 'images'), 11 | }, 12 | }, 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.svg$/i, 18 | loader: 'file-loader', 19 | options: { 20 | name: 'images/svgs/[name].[ext]', 21 | esModule: false, 22 | }, 23 | }, 24 | { 25 | test: /\.(png|jpe?g|gif)$/i, 26 | loader: 'file-loader', 27 | options: { 28 | name: 'images/[name].[ext]', 29 | esModule: false, 30 | }, 31 | }, 32 | { 33 | test: /\.html$/i, 34 | loader: 'html-loader', 35 | options: {}, 36 | }, 37 | ] 38 | }, 39 | 40 | plugins: [ 41 | new HtmlWebpackPlugin({ 42 | template: path.join(__dirname, 'fixtures', 'index.html'), 43 | }), 44 | new HtmlWebpackPlugin({ 45 | filename: path.resolve(webpackConfig.outputDir, 'index-allow-from-url.html'), 46 | template: path.join(__dirname, 'fixtures', 'index-allow-from-url.html') 47 | }), 48 | new HtmlWebpackInlineSVGPlugin({ 49 | allowFromUrl: true 50 | }), 51 | ], 52 | 53 | } 54 | -------------------------------------------------------------------------------- /spec/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var OUTPUT_DIR = path.join(__dirname, '../dist') 3 | 4 | module.exports = { 5 | 6 | outputDir: OUTPUT_DIR, 7 | 8 | options: { 9 | 10 | watch: false, 11 | 12 | entry: path.join(__dirname, 'fixtures', 'entry.js'), 13 | 14 | output: { 15 | path: OUTPUT_DIR, 16 | }, 17 | 18 | }, 19 | 20 | } 21 | -------------------------------------------------------------------------------- /spec/webpack.inline-all.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpackConfig = require('./webpack.base.config') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const HtmlWebpackInlineSVGPlugin = require('../') 5 | 6 | module.exports = { 7 | 8 | resolve: { 9 | alias: { 10 | 'img': path.join(__dirname, 'fixtures', 'images'), 11 | }, 12 | }, 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(svg)(\?.*)?$/, 18 | use: [ 19 | { 20 | loader: 'file-loader', 21 | options: { 22 | name: 'images/svgs/[name].[ext]', 23 | } 24 | } 25 | ], 26 | }, 27 | { 28 | test: /\.(png|jpe?g|gif)(\?.*)?$/, 29 | use: [ 30 | { 31 | loader: 'file-loader', 32 | options: { 33 | name: 'images/[name].[ext]', 34 | } 35 | } 36 | ], 37 | }, 38 | { 39 | test: /\.(html)$/, 40 | use: [ 41 | { 42 | loader: 'html-loader', 43 | options: {} 44 | } 45 | ], 46 | }, 47 | ] 48 | }, 49 | 50 | plugins: [ 51 | new HtmlWebpackPlugin({ 52 | filename: path.resolve(webpackConfig.outputDir, 'index-inline-all.html'), 53 | template: path.join(__dirname, 'fixtures', 'index-inline-all.html'), 54 | }), 55 | new HtmlWebpackInlineSVGPlugin({ 56 | inlineAll: true, 57 | }), 58 | ], 59 | 60 | } 61 | -------------------------------------------------------------------------------- /spec/webpack.post-emit.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpackConfig = require('./webpack.base.config') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const HtmlWebpackInlineSVGPlugin = require('../') 5 | 6 | module.exports = { 7 | 8 | resolve: { 9 | alias: { 10 | 'img': path.join(__dirname, 'fixtures', 'images'), 11 | }, 12 | }, 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.svg$/i, 18 | loader: 'file-loader', 19 | options: { 20 | name: 'images/svgs/[name].[ext]', 21 | esModule: false, 22 | }, 23 | }, 24 | { 25 | test: /\.(png|jpe?g|gif)$/i, 26 | loader: 'file-loader', 27 | options: { 28 | name: 'images/[name].[ext]', 29 | esModule: false, 30 | }, 31 | }, 32 | { 33 | test: /\.html$/i, 34 | loader: 'html-loader', 35 | options: {}, 36 | }, 37 | ] 38 | }, 39 | 40 | plugins: [ 41 | new HtmlWebpackPlugin({ 42 | template: path.join(__dirname, 'fixtures', 'index.html'), 43 | }), 44 | new HtmlWebpackPlugin({ 45 | filename: path.resolve(webpackConfig.outputDir, 'partial.html'), 46 | template: path.join(__dirname, 'fixtures', 'partial.html'), 47 | }), 48 | new HtmlWebpackInlineSVGPlugin(), 49 | ], 50 | 51 | } 52 | -------------------------------------------------------------------------------- /spec/webpack.pre-emit.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpackConfig = require('./webpack.base.config') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const HtmlWebpackInlineSVGPlugin = require('../') 5 | 6 | module.exports = { 7 | 8 | plugins: [ 9 | new HtmlWebpackPlugin({ 10 | filename: path.resolve(webpackConfig.outputDir, 'index.html'), 11 | template: path.join(__dirname, 'fixtures', 'index-pre-emit.html'), 12 | }), 13 | new HtmlWebpackPlugin({ 14 | filename: path.resolve(webpackConfig.outputDir, 'partial.html'), 15 | template: path.join(__dirname, 'fixtures', 'partial-pre-emit.html'), 16 | }), 17 | new HtmlWebpackInlineSVGPlugin({ 18 | runPreEmit: true, 19 | }), 20 | ], 21 | 22 | } 23 | -------------------------------------------------------------------------------- /spec/webpack.svgo.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpackConfig = require('./webpack.base.config') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const HtmlWebpackInlineSVGPlugin = require('../') 5 | 6 | module.exports = { 7 | 8 | resolve: { 9 | alias: { 10 | 'img': path.join(__dirname, 'fixtures', 'images'), 11 | }, 12 | }, 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(svg)(\?.*)?$/, 18 | use: [ 19 | { 20 | loader: 'file-loader', 21 | options: { 22 | name: 'images/svgs/[name].[ext]', 23 | } 24 | } 25 | ], 26 | }, 27 | { 28 | test: /\.(png|jpe?g|gif)(\?.*)?$/, 29 | use: [ 30 | { 31 | loader: 'file-loader', 32 | options: { 33 | name: 'images/[name].[ext]', 34 | } 35 | } 36 | ], 37 | }, 38 | { 39 | test: /\.(html)$/, 40 | use: [ 41 | { 42 | loader: 'html-loader', 43 | options: {} 44 | } 45 | ], 46 | }, 47 | ] 48 | }, 49 | 50 | plugins: [ 51 | new HtmlWebpackPlugin({ 52 | filename: path.resolve(webpackConfig.outputDir, 'index-svgo.html'), 53 | template: path.join(__dirname, 'fixtures', 'index-svgo.html') 54 | }), 55 | new HtmlWebpackInlineSVGPlugin({ 56 | svgoConfig: [ 57 | { 58 | removeXMLNS: true 59 | }, 60 | { 61 | removeTitle: false 62 | }, 63 | { 64 | removeComments: false 65 | } 66 | ] 67 | }), 68 | ], 69 | 70 | } 71 | --------------------------------------------------------------------------------