├── test ├── fixtures │ ├── sound.mp3 │ ├── video.mp4 │ ├── empty.html │ ├── example.pdf │ ├── example.vtt │ ├── site.webmanifest │ ├── entry.js │ ├── gallery.html │ ├── fallback.file.js │ ├── fallback.file.ts │ ├── module.file.js │ ├── posthtml.html │ ├── webpack-import-content.html │ ├── resolve-roots.html │ ├── style.file.css │ ├── fallback.file.json │ ├── script.js │ ├── broken-interpolation-syntax.html │ ├── roots.html │ ├── other-loader-query.html │ ├── nested.html │ ├── XHTML.js │ ├── empty.js │ ├── nested.js │ ├── roots.js │ ├── script.file.js │ ├── simple.js │ ├── sources.js │ ├── webpack-import-footer.html │ ├── absolute.js │ ├── template.js │ ├── webpack-import-header.html │ ├── file-protocol.js │ ├── image.png │ ├── invisible-space.html │ ├── pixel.png │ ├── 😀abc.png │ ├── example.ogg │ ├── favicon.ico │ ├── noscript.png │ ├── resolve-roots.js │ ├── webpack-import.js │ ├── webpackIgnore.js │ ├── image image.png │ ├── image.png.webp │ ├── invisible-space.js │ ├── broken-html-syntax.js │ ├── nested │ │ └── image3.png │ ├── other-loader-query.js │ ├── roots │ │ └── image2.png │ ├── broken-interpolation-syntax.js │ ├── preprocessor.hbs │ ├── broken-html-syntax.html │ ├── template.html │ ├── postprocessor.html │ ├── template-html.html │ ├── webpack-import-partial.html │ ├── XHTML.html │ ├── webpack.svg │ ├── browserconfig.xml │ ├── webpack-import.html │ ├── icons.svg │ ├── webpackIgnore.html │ ├── sources.html │ └── simple.html ├── helpers │ ├── getErrors.js │ ├── getWarnings.js │ ├── compile.js │ ├── readAssets.js │ ├── getModuleSource.js │ ├── svg-color-loader.js │ ├── index.js │ ├── normalizeErrors.js │ ├── readAsset.js │ ├── execute.js │ └── getCompiler.js ├── cjs.test.js ├── outputs │ └── url │ │ ├── https_raw.githubusercontent.com │ │ ├── webpack_html-loader_main_test_fixtures_image_2adc828f2e83bbe66759.png │ │ └── webpack-contrib_html-loader_master_test_fixtures_image_f3e23ef3ce98f665ebc5.png │ │ └── lock.json ├── esModule-option.test.js ├── runtime │ ├── getUrl.test.js │ └── __snapshots__ │ │ └── getUrl.test.js.snap ├── __snapshots__ │ ├── postprocessor-option.test.js.snap │ ├── preprocessor-option.test.js.snap │ └── validate-options.test.js.snap ├── postprocessor-option.test.js ├── preprocessor-option.test.js ├── minimize-option.test.js ├── validate-options.test.js ├── loader.test.js └── sources-option.test.js ├── .husky ├── pre-commit └── commit-msg ├── .gitattributes ├── .prettierignore ├── jest.config.js ├── src ├── plugins │ ├── index.js │ ├── minimizer-plugin.js │ └── sources-plugin.js ├── cjs.js ├── runtime │ └── getUrl.js ├── HtmlSourceError.js ├── index.js └── options.json ├── lint-staged.config.js ├── commitlint.config.js ├── eslint.config.mjs ├── .editorconfig ├── .gitignore ├── .github └── workflows │ ├── dependency-review.yml │ └── nodejs.yml ├── babel.config.js ├── LICENSE ├── .cspell.json ├── CONTRIBUTING.md ├── package.json ├── CHANGELOG.md └── README.md /test/fixtures/sound.mp3: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/video.mp4: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/empty.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/example.pdf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/example.vtt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /test/fixtures/site.webmanifest: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | commitlint --edit $1 2 | -------------------------------------------------------------------------------- /test/fixtures/entry.js: -------------------------------------------------------------------------------- 1 | console.log('HERE'); -------------------------------------------------------------------------------- /test/fixtures/gallery.html: -------------------------------------------------------------------------------- 1 |

Gallery

-------------------------------------------------------------------------------- /test/fixtures/fallback.file.js: -------------------------------------------------------------------------------- 1 | function test() {} 2 | -------------------------------------------------------------------------------- /test/fixtures/fallback.file.ts: -------------------------------------------------------------------------------- 1 | function test() {} 2 | -------------------------------------------------------------------------------- /test/fixtures/module.file.js: -------------------------------------------------------------------------------- 1 | function test() {} 2 | -------------------------------------------------------------------------------- /test/fixtures/posthtml.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/webpack-import-content.html: -------------------------------------------------------------------------------- 1 | Text -------------------------------------------------------------------------------- /test/fixtures/resolve-roots.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/style.file.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/fallback.file.json: -------------------------------------------------------------------------------- 1 | { 2 | "color" : "red" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/script.js: -------------------------------------------------------------------------------- 1 | export default './my-custom-script.js'; 2 | -------------------------------------------------------------------------------- /test/fixtures/broken-interpolation-syntax.html: -------------------------------------------------------------------------------- 1 |
${broken|||test}
2 | -------------------------------------------------------------------------------- /test/fixtures/roots.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/other-loader-query.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/nested.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | bin/* eol=lf 3 | yarn.lock -diff 4 | package-lock.json -diff -------------------------------------------------------------------------------- /test/fixtures/XHTML.js: -------------------------------------------------------------------------------- 1 | import xhtml from './XHTML.html'; 2 | 3 | export default xhtml; 4 | -------------------------------------------------------------------------------- /test/fixtures/empty.js: -------------------------------------------------------------------------------- 1 | import html from './empty.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/nested.js: -------------------------------------------------------------------------------- 1 | import html from './nested.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/roots.js: -------------------------------------------------------------------------------- 1 | import html from './roots.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/script.file.js: -------------------------------------------------------------------------------- 1 | function foo(arg) { 2 | return arg; 3 | } 4 | 5 | foo(1); 6 | -------------------------------------------------------------------------------- /test/fixtures/simple.js: -------------------------------------------------------------------------------- 1 | import html from './simple.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/sources.js: -------------------------------------------------------------------------------- 1 | import html from './sources.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/webpack-import-footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/absolute.js: -------------------------------------------------------------------------------- 1 | import html from './generated-1.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/template.js: -------------------------------------------------------------------------------- 1 | import html from './template.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/webpack-import-header.html: -------------------------------------------------------------------------------- 1 |
2 |

How to be a wizard

3 |
-------------------------------------------------------------------------------- /test/fixtures/file-protocol.js: -------------------------------------------------------------------------------- 1 | import html from './generated-2.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/image.png -------------------------------------------------------------------------------- /test/fixtures/invisible-space.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/pixel.png -------------------------------------------------------------------------------- /test/fixtures/😀abc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/😀abc.png -------------------------------------------------------------------------------- /test/fixtures/example.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/example.ogg -------------------------------------------------------------------------------- /test/fixtures/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/favicon.ico -------------------------------------------------------------------------------- /test/fixtures/noscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/noscript.png -------------------------------------------------------------------------------- /test/fixtures/resolve-roots.js: -------------------------------------------------------------------------------- 1 | import html from './resolve-roots.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/webpack-import.js: -------------------------------------------------------------------------------- 1 | import html from './webpack-import.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/webpackIgnore.js: -------------------------------------------------------------------------------- 1 | import html from './webpackIgnore.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/image image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/image image.png -------------------------------------------------------------------------------- /test/fixtures/image.png.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/image.png.webp -------------------------------------------------------------------------------- /test/fixtures/invisible-space.js: -------------------------------------------------------------------------------- 1 | import html from './invisible-space.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures 5 | CHANGELOG.md 6 | test/outputs/url/** 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testTimeout: 20000, 3 | collectCoverageFrom: ["src/**/*.js"], 4 | }; 5 | -------------------------------------------------------------------------------- /test/fixtures/broken-html-syntax.js: -------------------------------------------------------------------------------- 1 | import html from './broken-html-syntax.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/nested/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/nested/image3.png -------------------------------------------------------------------------------- /test/fixtures/other-loader-query.js: -------------------------------------------------------------------------------- 1 | import html from './other-loader-query.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/roots/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/roots/image2.png -------------------------------------------------------------------------------- /test/fixtures/broken-interpolation-syntax.js: -------------------------------------------------------------------------------- 1 | import html from './broken-interpolation-syntax.html'; 2 | 3 | export default html; 4 | -------------------------------------------------------------------------------- /test/fixtures/preprocessor.hbs: -------------------------------------------------------------------------------- 1 |
2 |

{{firstname}} {{lastname}}

3 | alt 4 |
5 | -------------------------------------------------------------------------------- /src/plugins/index.js: -------------------------------------------------------------------------------- 1 | export { default as minimizerPlugin } from "./minimizer-plugin"; 2 | export { default as sourcesPlugin } from "./sources-plugin"; 3 | -------------------------------------------------------------------------------- /test/helpers/getErrors.js: -------------------------------------------------------------------------------- 1 | import normalizeErrors from "./normalizeErrors"; 2 | 3 | export default (stats) => normalizeErrors(stats.compilation.errors).sort(); 4 | -------------------------------------------------------------------------------- /test/helpers/getWarnings.js: -------------------------------------------------------------------------------- 1 | import normalizeErrors from "./normalizeErrors"; 2 | 3 | export default (stats) => normalizeErrors(stats.compilation.warnings).sort(); 4 | -------------------------------------------------------------------------------- /test/cjs.test.js: -------------------------------------------------------------------------------- 1 | import src from "../src"; 2 | import cjs from "../src/cjs"; 3 | 4 | describe("cjs", () => { 5 | it("should exported", () => { 6 | expect(cjs).toEqual(src); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/cjs.js: -------------------------------------------------------------------------------- 1 | const loader = require("./index"); 2 | 3 | module.exports = loader.default; 4 | module.exports.defaultMinimizerOptions = loader.defaultMinimizerOptions; 5 | module.exports.raw = loader.raw; 6 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*": [ 3 | "prettier --cache --write --ignore-unknown", 4 | "cspell --cache --no-must-find-files", 5 | ], 6 | "*.js": ["eslint --cache --fix"], 7 | }; 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "header-max-length": [0], 5 | "body-max-line-length": [0], 6 | "footer-max-line-length": [0], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import configs from "eslint-config-webpack/configs.js"; 3 | 4 | export default defineConfig([ 5 | { 6 | extends: [configs["recommended-dirty"]], 7 | }, 8 | ]); 9 | -------------------------------------------------------------------------------- /test/fixtures/broken-html-syntax.html: -------------------------------------------------------------------------------- 1 | Text < img src="image.png" > 2 | Text < 3 | Text > 4 | 5 | boohay 6 | <<<<>foo 7 | >>< 2 | new Promise((resolve, reject) => { 3 | compiler.run((error, stats) => { 4 | if (error) { 5 | return reject(error); 6 | } 7 | 8 | return resolve(stats); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/runtime/getUrl.js: -------------------------------------------------------------------------------- 1 | module.exports = (url, maybeNeedQuotes) => { 2 | if (!url) { 3 | return url; 4 | } 5 | 6 | url = String(url); 7 | 8 | if (maybeNeedQuotes && /[\t\n\f\r "'=<>`]/.test(url)) { 9 | return `"${url}"`; 10 | } 11 | 12 | return url; 13 | }; 14 | -------------------------------------------------------------------------------- /test/outputs/url/https_raw.githubusercontent.com/webpack_html-loader_main_test_fixtures_image_2adc828f2e83bbe66759.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/outputs/url/https_raw.githubusercontent.com/webpack_html-loader_main_test_fixtures_image_2adc828f2e83bbe66759.png -------------------------------------------------------------------------------- /test/fixtures/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Something about the \` character

4 | 5 |

#{number} {customer}

6 |

{title}

7 | 8 | -------------------------------------------------------------------------------- /test/outputs/url/https_raw.githubusercontent.com/webpack-contrib_html-loader_master_test_fixtures_image_f3e23ef3ce98f665ebc5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/html-loader/main/test/outputs/url/https_raw.githubusercontent.com/webpack-contrib_html-loader_master_test_fixtures_image_f3e23ef3ce98f665ebc5.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [test/fixtures/*.html] 15 | insert_final_newline = false 16 | -------------------------------------------------------------------------------- /test/helpers/readAssets.js: -------------------------------------------------------------------------------- 1 | import readAsset from "./readAsset"; 2 | 3 | export default function readAssets(compiler, stats) { 4 | const assets = {}; 5 | 6 | for (const asset of Object.keys(stats.compilation.assets)) { 7 | assets[asset] = readAsset(asset, compiler, stats); 8 | } 9 | 10 | return assets; 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/minimizer-plugin.js: -------------------------------------------------------------------------------- 1 | import { minify } from "html-minifier-terser"; 2 | 3 | export default (options) => 4 | async function process(html) { 5 | try { 6 | html = await minify(html, options.minimize); 7 | } catch (error) { 8 | options.errors.push(error); 9 | } 10 | 11 | return html; 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/postprocessor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
<%= require('./gallery.html').default %>
6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | 4 | *.sw* 5 | 6 | npm-debug.log 7 | logs 8 | *.log 9 | npm-debug.log* 10 | .eslintcache 11 | .cspellcache 12 | /coverage 13 | /dist 14 | /local 15 | /reports 16 | /node_modules 17 | .DS_Store 18 | Thumbs.db 19 | .idea 20 | *.iml 21 | .vscode 22 | *.sublime-project 23 | *.sublime-workspace 24 | /test/fixtures/generated-2.html 25 | -------------------------------------------------------------------------------- /test/helpers/getModuleSource.js: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from "node:url"; 2 | 3 | export default (id, stats) => { 4 | const { modules } = stats.toJson({ source: true }); 5 | const module = modules.find((m) => m.name === id); 6 | let { source } = module; 7 | 8 | source = source.replace(pathToFileURL(process.cwd()), "file:////"); 9 | 10 | return source; 11 | }; 12 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: "Dependency Review" 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Checkout Repository" 12 | uses: actions/checkout@v5 13 | - name: "Dependency Review" 14 | uses: actions/dependency-review-action@v4 15 | -------------------------------------------------------------------------------- /test/fixtures/template-html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Title 9 | 10 | 11 |
Text
12 | image 13 | 14 | -------------------------------------------------------------------------------- /test/fixtures/webpack-import-partial.html: -------------------------------------------------------------------------------- 1 |
2 | Example 3 |
4 | 5 |
6 |
7 |
8 | 9 |
10 | 13 |
14 | 15 |
16 |
-------------------------------------------------------------------------------- /test/outputs/url/lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://github.com/webpack/css-loader/blob/main/src/index.js": { "integrity": "sha512-aZkLHvP1jheyEMQt+e8pjg6SII996vPPaxjEk/R4cz/MoR/+JHLEzdc7/6/5umZP1ogZvSjP5SYFaqTWsxgDGQ==", "contentType": "text/html; charset=utf-8" }, 3 | "https://raw.githubusercontent.com/webpack/html-loader/main/test/fixtures/image.png": { "integrity": "sha512-bHqIPBYwzPsVLYcTDqJzwgvIaxLjmezufiCVXAMI0Naelf3eWVdydMA40hXbSuB0dZCGjCepuGaI7Ze8kLM+Ew==", "contentType": "image/png" }, 4 | "version": 1 5 | } 6 | -------------------------------------------------------------------------------- /test/helpers/svg-color-loader.js: -------------------------------------------------------------------------------- 1 | const querystring = require("node:querystring"); 2 | 3 | module.exports = function loader() { 4 | const query = querystring.parse(this.resourceQuery.slice(1)); 5 | 6 | if (typeof query.color === "undefined" || query.color !== "#BAAFDB?") { 7 | throw new Error(`Error, 'color' is '${query.color}'`); 8 | } 9 | 10 | return 'export default "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";'; 11 | }; 12 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | export { default as compile } from "./compile"; 2 | export { default as getCompiler } from "./getCompiler"; 3 | export { default as execute } from "./execute"; 4 | export { default as getModuleSource } from "./getModuleSource"; 5 | export { default as getErrors } from "./getErrors"; 6 | export { default as normalizeErrors } from "./normalizeErrors"; 7 | export { default as getWarnings } from "./getWarnings"; 8 | export { default as readsAssets } from "./readAssets"; 9 | export { default as readAsset } from "./readAsset"; 10 | -------------------------------------------------------------------------------- /test/helpers/normalizeErrors.js: -------------------------------------------------------------------------------- 1 | function removeCWD(str) { 2 | const isWin = process.platform === "win32"; 3 | let cwd = process.cwd(); 4 | 5 | if (isWin) { 6 | str = str.replaceAll("\\", "/"); 7 | 8 | cwd = cwd.replaceAll("\\", "/"); 9 | } 10 | 11 | return str 12 | .replaceAll(new RegExp(cwd, "g"), "") 13 | .replace(/\(from (.*)\)/, "(from /path/to/file.js)"); 14 | } 15 | 16 | export default (errors) => 17 | errors.map((error) => 18 | removeCWD(error.toString().split("\n").slice(0, 2).join("\n")), 19 | ); 20 | -------------------------------------------------------------------------------- /test/fixtures/XHTML.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Title of document 6 | 7 | 8 | 9 | some content here... 10 | 11 |
12 | 13 | Visit our HTML tutorial 14 | Visit our HTML tutorial 15 | 16 | 17 | 18 | Text 19 | Text 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/helpers/readAsset.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | export default (asset, compiler, stats) => { 4 | const usedFs = compiler.outputFileSystem; 5 | const outputPath = stats.compilation.outputOptions.path; 6 | 7 | let data = ""; 8 | let targetFile = asset; 9 | 10 | const queryStringIdx = targetFile.indexOf("?"); 11 | 12 | if (queryStringIdx >= 0) { 13 | targetFile = targetFile.slice(0, queryStringIdx); 14 | } 15 | 16 | try { 17 | data = usedFs.readFileSync(path.join(outputPath, targetFile)).toString(); 18 | } catch (error) { 19 | data = error.toString(); 20 | } 21 | 22 | return data; 23 | }; 24 | -------------------------------------------------------------------------------- /test/fixtures/webpack.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const MIN_BABEL_VERSION = 7; 2 | 3 | module.exports = (api) => { 4 | api.assertVersion(MIN_BABEL_VERSION); 5 | api.cache(true); 6 | 7 | return { 8 | presets: [ 9 | [ 10 | "@babel/preset-env", 11 | { 12 | targets: { 13 | node: "18.12.0", 14 | }, 15 | }, 16 | ], 17 | ], 18 | overrides: [ 19 | { 20 | test: "./src/runtime", 21 | presets: [ 22 | [ 23 | "@babel/preset-env", 24 | { 25 | targets: { 26 | node: "0.14", 27 | }, 28 | }, 29 | ], 30 | ], 31 | }, 32 | ], 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /test/helpers/execute.js: -------------------------------------------------------------------------------- 1 | import Module from "node:module"; 2 | import path from "node:path"; 3 | 4 | const parentModule = module; 5 | 6 | function replaceAbsolutePath(data) { 7 | return typeof data === "string" 8 | ? data.replaceAll(/file:\/\/\/(\D:\/)?/gi, "replaced_file_protocol_/") 9 | : data; 10 | } 11 | 12 | export default (code) => { 13 | const resource = "test.js"; 14 | const module = new Module(resource, parentModule); 15 | 16 | module.paths = Module._nodeModulePaths( 17 | path.resolve(__dirname, "../fixtures"), 18 | ); 19 | module.filename = resource; 20 | 21 | module._compile( 22 | `${code};module.exports = ___TEST___.default ?___TEST___.default : ___TEST___;`, 23 | resource, 24 | ); 25 | 26 | return replaceAbsolutePath(module.exports); 27 | }; 28 | -------------------------------------------------------------------------------- /test/fixtures/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #000000 10 | 11 | 12 | 13 | 14 | 15 | 30 16 | 1 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/fixtures/webpack-import.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | BeforeHeaderTextAfterHeaderText 4 |
    5 |
  1. Grow a long, majestic beard.
  2. 6 |
  3. Wear a tall, pointed hat.
  4. 7 |
  5. Have I mentioned the beard?
  6. 8 |
9 | BeforeFooterTextAfterFooterText 10 | TextBeforeOpenDiv
TextAfterOpenDivTextBeforeCloseDiv
TextAfterCloseDiv 11 |
12 |
13 | 14 |
15 | test 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 | BEFORE 28 | 29 |
Header
30 |
Header
31 |
Footer
32 | Custom 33 |
34 | AFTER 35 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en,en-gb", 4 | "words": [ 5 | "lastname", 6 | "firstname", 7 | "memfs", 8 | "ASCIIC", 9 | "xlink", 10 | "imagesrcset", 11 | "Requestable", 12 | "Itemprop", 13 | "itemprop", 14 | "layoutimage", 15 | "installurl", 16 | "embedurl", 17 | "duringmedia", 18 | "downloadurl", 19 | "contenturl", 20 | "thumbnailurl", 21 | "tileimage", 22 | "Rels", 23 | "precomposed", 24 | "cssnano", 25 | "requestify", 26 | "substeps", 27 | "srcset", 28 | "scrset", 29 | "plusplus", 30 | "webp", 31 | "Webp", 32 | "layoutimage", 33 | "msapplication", 34 | "hspace", 35 | "vspace", 36 | "jsbeautify", 37 | "Gitter", 38 | "commitlint", 39 | "postprocessor", 40 | "eslintcache", 41 | "cspellcache" 42 | ], 43 | "ignorePaths": [ 44 | "CHANGELOG.md", 45 | "package.json", 46 | "dist/**", 47 | "**/__snapshots__/**", 48 | "package-lock.json", 49 | "*.xml", 50 | "*.ogg", 51 | "*.html", 52 | "*.webp", 53 | "node_modules", 54 | "coverage", 55 | "*.log", 56 | "test/outputs/url/**" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /src/HtmlSourceError.js: -------------------------------------------------------------------------------- 1 | function getIndices(value) { 2 | const result = []; 3 | let index = value.indexOf("\n"); 4 | 5 | while (index !== -1) { 6 | result.push(index + 1); 7 | index = value.indexOf("\n", index + 1); 8 | } 9 | 10 | result.push(value.length + 1); 11 | 12 | return result; 13 | } 14 | 15 | function offsetToPosition(source, offset) { 16 | let index = -1; 17 | const indices = getIndices(source); 18 | const { length } = indices; 19 | 20 | if (offset < 0) { 21 | return {}; 22 | } 23 | 24 | while (++index < length) { 25 | if (indices[index] > offset) { 26 | return { 27 | line: index + 1, 28 | column: offset - (indices[index - 1] || 0) + 1, 29 | offset, 30 | }; 31 | } 32 | } 33 | 34 | return {}; 35 | } 36 | 37 | export default class HtmlSourceError extends Error { 38 | constructor(error, startOffset, endOffset, source) { 39 | super(error); 40 | 41 | this.name = "HtmlSourceError"; 42 | this.message = `${this.name}: ${this.message}`; 43 | this.startOffset = startOffset; 44 | this.endOffset = endOffset; 45 | this.source = source; 46 | 47 | const startPosition = offsetToPosition(source, this.startOffset); 48 | const endPosition = offsetToPosition(source, this.endOffset); 49 | 50 | this.message += ` (From line ${startPosition.line}, column ${startPosition.column}; to line ${endPosition.line}, column ${endPosition.column})`; 51 | 52 | // We don't need stack 53 | this.stack = false; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | From opening a bug report to creating a pull request: every contribution is 4 | appreciated and welcome. If you're planning to implement a new feature or change 5 | the api please create an issue first. This way we can ensure that your precious 6 | work is not in vain. 7 | 8 | ## Issues 9 | 10 | Most of the time, if webpack is not working correctly for you it is a simple configuration issue. 11 | 12 | If you are having difficulty, please search the [StackOverflow with the webpack tag](http://stackoverflow.com/tags/webpack) for questions related 13 | to the `html-loader`. If you can find an answer to your issue, please post a question in [StackOverflow](http://stackoverflow.com/tags/webpack) or 14 | the [webpack Gitter](https://gitter.im/webpack/webpack) and include both your webpack & html-loader versions. 15 | 16 | **If you have discovered a bug or have a feature suggestion, feel free to create an issue on Github.** 17 | 18 | ## Setup 19 | 20 | ```bash 21 | git clone https://github.com/webpack/html-loader.git 22 | cd html-loader 23 | npm install 24 | ``` 25 | 26 | To run the entire test suite use: 27 | 28 | ```bash 29 | npm test 30 | ``` 31 | 32 | ## Submitting Changes 33 | 34 | After getting some feedback, push to your fork and submit a pull request. We 35 | may suggest some changes or improvements or alternatives, but for small changes 36 | your pull request should be accepted quickly. 37 | 38 | Some things that will increase the chance that your pull request is accepted: 39 | 40 | - Write tests 41 | - Follow the existing Webpack coding style defined in the eslint jsbeautify and editor config rules. 42 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 43 | -------------------------------------------------------------------------------- /test/esModule-option.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | compile, 3 | execute, 4 | getCompiler, 5 | getErrors, 6 | getModuleSource, 7 | getWarnings, 8 | readAsset, 9 | } from "./helpers"; 10 | 11 | describe("'esModule' option", () => { 12 | it("should use a CommonJS export by default", async () => { 13 | const compiler = getCompiler("simple.js"); 14 | const stats = await compile(compiler); 15 | 16 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 17 | expect( 18 | execute(readAsset("main.bundle.js", compiler, stats)), 19 | ).toMatchSnapshot("result"); 20 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 21 | expect(getErrors(stats)).toMatchSnapshot("errors"); 22 | }); 23 | 24 | it('should use a CommonJS export when the value is "false"', async () => { 25 | const compiler = getCompiler("simple.js", { esModule: false }); 26 | const stats = await compile(compiler); 27 | 28 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 29 | expect( 30 | execute(readAsset("main.bundle.js", compiler, stats)), 31 | ).toMatchSnapshot("result"); 32 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 33 | expect(getErrors(stats)).toMatchSnapshot("errors"); 34 | }); 35 | 36 | it('should use an ES module export when the value is "true"', async () => { 37 | const compiler = getCompiler("simple.js", { esModule: true }); 38 | const stats = await compile(compiler); 39 | 40 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 41 | expect( 42 | execute(readAsset("main.bundle.js", compiler, stats)), 43 | ).toMatchSnapshot("result"); 44 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 45 | expect(getErrors(stats)).toMatchSnapshot("errors"); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/runtime/getUrl.test.js: -------------------------------------------------------------------------------- 1 | import getUrl from "../../src/runtime/getUrl"; 2 | 3 | describe("getUrl", () => { 4 | it("should work", () => { 5 | expect(getUrl(true)).toMatchSnapshot(); 6 | expect(getUrl(null)).toMatchSnapshot(); 7 | 8 | expect(getUrl(undefined)).toMatchSnapshot(); 9 | expect(getUrl("image.png")).toMatchSnapshot(); 10 | expect(getUrl("image\timage.png")).toMatchSnapshot(); 11 | expect(getUrl("image\nimage.png")).toMatchSnapshot(); 12 | expect(getUrl("image\fimage.png")).toMatchSnapshot(); 13 | expect(getUrl("image\rimage.png")).toMatchSnapshot(); 14 | expect(getUrl("image image.png")).toMatchSnapshot(); 15 | expect(getUrl('image"image.png')).toMatchSnapshot(); 16 | expect(getUrl("image'image.png")).toMatchSnapshot(); 17 | expect(getUrl("image=image.png")).toMatchSnapshot(); 18 | expect(getUrl("image>image.png")).toMatchSnapshot(); 19 | expect(getUrl("imageimage.png", true)).toMatchSnapshot(); 31 | expect(getUrl("imageimage.png"`; 34 | 35 | exports[`getUrl should work 14`] = `"imageimage.png""`; 64 | 65 | exports[`getUrl should work 26`] = `""image { 7 | const fullConfig = { 8 | mode: "development", 9 | devtool: config.devtool || false, 10 | context: path.resolve(__dirname, "../fixtures"), 11 | entry: path.resolve(__dirname, "../fixtures", fixture), 12 | output: { 13 | path: path.resolve(__dirname, "../outputs"), 14 | filename: "[name].bundle.js", 15 | chunkFilename: "[name].chunk.js", 16 | chunkLoading: "require", 17 | publicPath: "/webpack/public/path/", 18 | library: "___TEST___", 19 | assetModuleFilename: "[name]-[contenthash][ext]", 20 | hashFunction: "xxhash64", 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(html|hbs)$/i, 26 | rules: [ 27 | { 28 | loader: path.resolve(__dirname, "../../src"), 29 | options: loaderOptions || {}, 30 | }, 31 | ], 32 | }, 33 | { 34 | resourceQuery: /\?url$/, 35 | type: "asset/inline", 36 | }, 37 | { 38 | test: /\.(png|jpg|gif|svg|ico|eot|ttf|woff|woff2|ogg|pdf|vtt|webp|xml|webmanifest|mp3|mp4)$/i, 39 | resourceQuery: /^(?!.*\?url).*$/, 40 | type: "asset/resource", 41 | }, 42 | { 43 | test: /\.file.css$/i, 44 | type: "asset/resource", 45 | }, 46 | { 47 | test: /\.file.js$/i, 48 | type: "asset/resource", 49 | }, 50 | ], 51 | }, 52 | resolve: { 53 | alias: { 54 | aliasImg: path.resolve(__dirname, "../fixtures/image.png"), 55 | aliasImageWithSpace: path.resolve( 56 | __dirname, 57 | "../fixtures/image image.png", 58 | ), 59 | }, 60 | }, 61 | plugins: [], 62 | ...config, 63 | }; 64 | 65 | const compiler = webpack(fullConfig); 66 | 67 | if (!config.outputFileSystem) { 68 | compiler.outputFileSystem = createFsFromVolume(new Volume()); 69 | } 70 | 71 | return compiler; 72 | }; 73 | -------------------------------------------------------------------------------- /test/fixtures/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import schema from "./options.json"; 2 | import { minimizerPlugin, sourcesPlugin } from "./plugins"; 3 | import { 4 | convertToTemplateLiteral, 5 | getExportCode, 6 | getImportCode, 7 | getModuleCode, 8 | normalizeOptions, 9 | pluginRunner, 10 | supportTemplateLiteral, 11 | } from "./utils"; 12 | 13 | export default async function loader(content) { 14 | const rawOptions = this.getOptions(schema); 15 | const options = normalizeOptions(rawOptions, this); 16 | 17 | if (options.preprocessor) { 18 | content = await options.preprocessor(content, this); 19 | } 20 | 21 | const plugins = []; 22 | const errors = []; 23 | const imports = []; 24 | const replacements = []; 25 | 26 | let isSupportAbsoluteURL = false; 27 | 28 | // TODO enable by default in the next major release 29 | if ( 30 | this._compilation && 31 | this._compilation.options && 32 | this._compilation.options.experiments && 33 | this._compilation.options.experiments.buildHttp 34 | ) { 35 | isSupportAbsoluteURL = true; 36 | } 37 | 38 | if (options.sources) { 39 | plugins.push( 40 | sourcesPlugin({ 41 | isSupportAbsoluteURL, 42 | isSupportDataURL: options.esModule, 43 | sources: options.sources, 44 | resourcePath: this.resourcePath, 45 | context: this.context, 46 | imports, 47 | errors, 48 | replacements, 49 | }), 50 | ); 51 | } 52 | 53 | if (options.minimize) { 54 | plugins.push(minimizerPlugin({ minimize: options.minimize, errors })); 55 | } 56 | 57 | let { html } = await pluginRunner(plugins).process(content); 58 | 59 | for (const error of errors) { 60 | this.emitError(error instanceof Error ? error : new Error(error)); 61 | } 62 | 63 | const isTemplateLiteralSupported = supportTemplateLiteral(this); 64 | 65 | html = ( 66 | isTemplateLiteralSupported 67 | ? convertToTemplateLiteral(html) 68 | : JSON.stringify(html) 69 | ) 70 | // Invalid in JavaScript but valid HTML 71 | .replaceAll(/[\u2028\u2029]/g, (str) => 72 | str === "\u2029" ? "\\u2029" : "\\u2028", 73 | ); 74 | 75 | if (options.postprocessor) { 76 | html = await options.postprocessor(html, this); 77 | } 78 | 79 | const importCode = getImportCode(html, imports, options); 80 | const moduleCode = getModuleCode(html, replacements, this, { 81 | esModule: options.esModule, 82 | isTemplateLiteralSupported, 83 | }); 84 | const exportCode = getExportCode(html, options); 85 | 86 | return `${importCode}${moduleCode}${exportCode}`; 87 | } 88 | 89 | export { defaultMinimizerOptions } from "./utils"; 90 | -------------------------------------------------------------------------------- /src/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "HTML Loader options", 3 | "type": "object", 4 | "definitions": { 5 | "Source": { 6 | "anyOf": [ 7 | { 8 | "type": "object", 9 | "properties": { 10 | "tag": { 11 | "type": "string", 12 | "minLength": 1 13 | }, 14 | "attribute": { 15 | "type": "string", 16 | "minLength": 1 17 | }, 18 | "type": { 19 | "enum": ["src", "srcset"] 20 | }, 21 | "filter": { 22 | "instanceof": "Function" 23 | } 24 | }, 25 | "required": ["attribute", "type"], 26 | "additionalProperties": false 27 | }, 28 | { 29 | "enum": ["..."] 30 | } 31 | ] 32 | }, 33 | "SourcesList": { 34 | "type": "array", 35 | "items": { 36 | "$ref": "#/definitions/Source" 37 | }, 38 | "minItems": 1, 39 | "uniqueItems": true 40 | } 41 | }, 42 | "properties": { 43 | "preprocessor": { 44 | "instanceof": "Function", 45 | "description": "Allows pre-processing of content before handling.", 46 | "link": "https://github.com/webpack/html-loader#preprocessor" 47 | }, 48 | "postprocessor": { 49 | "instanceof": "Function", 50 | "description": "Allows post-processing of content before handling.", 51 | "link": "https://github.com/webpack/html-loader#postprocessor" 52 | }, 53 | "sources": { 54 | "anyOf": [ 55 | { "type": "boolean" }, 56 | { 57 | "type": "object", 58 | "properties": { 59 | "list": { 60 | "$ref": "#/definitions/SourcesList" 61 | }, 62 | "urlFilter": { 63 | "instanceof": "Function" 64 | }, 65 | "scriptingEnabled": { 66 | "type": "boolean" 67 | } 68 | }, 69 | "additionalProperties": false 70 | } 71 | ], 72 | "description": "By default every loadable attributes (for example - ) is imported (const img = require('./image.png'). You may need to specify loaders for images in your configuration.", 73 | "link": "https://github.com/webpack/html-loader#sources" 74 | }, 75 | "minimize": { 76 | "anyOf": [{ "type": "boolean" }, { "type": "object" }], 77 | "description": "Tell html-loader to minimize HTML.", 78 | "link": "https://github.com/webpack/html-loader#minimize" 79 | }, 80 | "esModule": { 81 | "type": "boolean", 82 | "description": "Enable or disable ES modules syntax.", 83 | "link": "https://github.com/webpack/html-loader#esmodule" 84 | } 85 | }, 86 | "additionalProperties": false 87 | } 88 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: html-loader 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | name: Lint - ${{ matrix.os }} - Node v${{ matrix.node-version }} 19 | 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | node-version: [lts/*] 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | concurrency: 31 | group: lint-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} 32 | cancel-in-progress: true 33 | 34 | steps: 35 | - uses: actions/checkout@v5 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Use Node.js ${{ matrix.node-version }} 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | cache: "npm" 44 | 45 | - name: Install dependencies 46 | run: npm ci 47 | 48 | - name: Lint 49 | run: npm run lint 50 | 51 | # - name: Security audit 52 | # run: npm run security 53 | 54 | - name: Validate PR commits with commitlint 55 | if: github.event_name == 'pull_request' 56 | run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose 57 | 58 | test: 59 | name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Webpack ${{ matrix.webpack-version }} 60 | 61 | strategy: 62 | matrix: 63 | os: [ubuntu-latest, windows-latest, macos-latest] 64 | node-version: [18.x, 20.x, 22.x, 24.x] 65 | webpack-version: [latest] 66 | 67 | runs-on: ${{ matrix.os }} 68 | 69 | concurrency: 70 | group: test-${{ matrix.os }}-v${{ matrix.node-version }}-${{ matrix.webpack-version }}-${{ github.ref }} 71 | cancel-in-progress: true 72 | 73 | steps: 74 | - name: Setup Git 75 | if: matrix.os == 'windows-latest' 76 | run: git config --global core.autocrlf input 77 | 78 | - uses: actions/checkout@v5 79 | 80 | - name: Use Node.js ${{ matrix.node-version }} 81 | uses: actions/setup-node@v4 82 | with: 83 | node-version: ${{ matrix.node-version }} 84 | cache: "npm" 85 | 86 | - name: Install dependencies 87 | run: npm ci 88 | 89 | - name: Install webpack ${{ matrix.webpack-version }} 90 | if: matrix.webpack-version != 'latest' 91 | run: npm i webpack@${{ matrix.webpack-version }} 92 | 93 | - name: Run tests for webpack version ${{ matrix.webpack-version }} 94 | run: npm run test:coverage -- --ci 95 | 96 | - name: Submit coverage data to codecov 97 | uses: codecov/codecov-action@v5 98 | with: 99 | token: ${{ secrets.CODECOV_TOKEN }} 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-loader", 3 | "version": "5.1.0", 4 | "description": "Html loader module for webpack", 5 | "keywords": [ 6 | "webpack", 7 | "html", 8 | "loader", 9 | "bundler" 10 | ], 11 | "homepage": "https://github.com/webpack/html-loader", 12 | "bugs": "https://github.com/webpack/html-loader/issues", 13 | "repository": "webpack/html-loader", 14 | "funding": { 15 | "type": "opencollective", 16 | "url": "https://opencollective.com/webpack" 17 | }, 18 | "license": "MIT", 19 | "author": "Tobias Koppers @sokra", 20 | "main": "dist/cjs.js", 21 | "files": [ 22 | "dist" 23 | ], 24 | "scripts": { 25 | "start": "npm run build -- -w", 26 | "clean": "del-cli dist", 27 | "validate:runtime": "es-check es5 \"dist/runtime/**/*.js\"", 28 | "prebuild": "npm run clean", 29 | "build": "cross-env NODE_ENV=production babel src -d dist --copy-files", 30 | "postbuild": "npm run validate:runtime", 31 | "commitlint": "commitlint --from=main", 32 | "security": "npm audit --production", 33 | "lint:prettier": "prettier --cache --list-different .", 34 | "lint:js": "eslint --cache .", 35 | "lint:spelling": "cspell --cache --no-must-find-files --quiet \"**/*.*\"", 36 | "lint": "npm-run-all -l -p \"lint:**\"", 37 | "fix:js": "npm run lint:js -- --fix", 38 | "fix:prettier": "npm run lint:prettier -- --write", 39 | "fix": "npm-run-all -l fix:js fix:prettier", 40 | "test:only": "cross-env NODE_ENV=test jest", 41 | "test:watch": "npm run test:only -- --watch", 42 | "test:coverage": "npm run test:only -- --coverage", 43 | "pretest": "npm run lint", 44 | "test": "npm run test:coverage", 45 | "prepare": "husky && npm run build", 46 | "release": "standard-version" 47 | }, 48 | "dependencies": { 49 | "html-minifier-terser": "^7.2.0", 50 | "parse5": "^7.1.2" 51 | }, 52 | "devDependencies": { 53 | "@babel/cli": "^7.24.8", 54 | "@babel/core": "^7.25.2", 55 | "@babel/preset-env": "^7.25.3", 56 | "@commitlint/cli": "^19.3.0", 57 | "@commitlint/config-conventional": "^19.2.2", 58 | "@eslint/js": "^9.32.0", 59 | "@eslint/markdown": "^7.1.0", 60 | "@stylistic/eslint-plugin": "^5.2.2", 61 | "babel-jest": "^30.0.0", 62 | "cross-env": "^7.0.3", 63 | "cspell": "^8.13.1", 64 | "del": "^7.1.0", 65 | "del-cli": "^5.1.0", 66 | "es-check": "^9.1.4", 67 | "eslint": "^9.31.0", 68 | "eslint-config-prettier": "^10.1.8", 69 | "eslint-config-webpack": "^4.4.2", 70 | "eslint-plugin-import": "^2.32.0", 71 | "eslint-plugin-jest": "^29.0.1", 72 | "eslint-plugin-n": "^17.21.0", 73 | "eslint-plugin-prettier": "^5.5.3", 74 | "eslint-plugin-unicorn": "^60.0.0", 75 | "globals": "^16.3.0", 76 | "handlebars": "^4.7.8", 77 | "html-webpack-plugin": "^5.6.0", 78 | "husky": "^9.1.4", 79 | "jest": "^30.0.0", 80 | "lint-staged": "^15.2.8", 81 | "memfs": "^4.11.1", 82 | "npm-run-all": "^4.1.5", 83 | "posthtml": "^0.16.6", 84 | "posthtml-webp": "^2.2.0", 85 | "prettier": "^3.3.3", 86 | "standard-version": "^9.5.0", 87 | "typescript-eslint": "^8.38.0", 88 | "unescape-unicode": "^0.2.0", 89 | "webpack": "^5.93.0" 90 | }, 91 | "peerDependencies": { 92 | "webpack": "^5.0.0" 93 | }, 94 | "engines": { 95 | "node": ">= 18.12.0" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/__snapshots__/postprocessor-option.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`'postprocess' option should work with async "postprocessor" function option: errors 1`] = `[]`; 4 | 5 | exports[`'postprocess' option should work with async "postprocessor" function option: module 1`] = ` 6 | "// Imports 7 | var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png", import.meta.url); 8 | // Module 9 | var code = \`
10 |

{{firstname}} {{lastname}}

11 | alt 12 |
13 | \`; 14 | // Exports 15 | export default code;" 16 | `; 17 | 18 | exports[`'postprocess' option should work with async "postprocessor" function option: result 1`] = ` 19 | "
20 |

{{firstname}} {{lastname}}

21 | alt 22 |
23 | " 24 | `; 25 | 26 | exports[`'postprocess' option should work with async "postprocessor" function option: warnings 1`] = `[]`; 27 | 28 | exports[`'postprocess' option should work with the "postprocessor" option #1: errors 1`] = `[]`; 29 | 30 | exports[`'postprocess' option should work with the "postprocessor" option #1: module 1`] = ` 31 | "// Imports 32 | var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png", import.meta.url); 33 | // Module 34 | var code = "\\n\\n\\n\\n
" + require('./gallery.html').default + "
\\n\\n"; 35 | // Exports 36 | export default code;" 37 | `; 38 | 39 | exports[`'postprocess' option should work with the "postprocessor" option #1: result 1`] = ` 40 | " 41 | 42 | 43 | 44 |

Gallery

45 | 46 | " 47 | `; 48 | 49 | exports[`'postprocess' option should work with the "postprocessor" option #1: warnings 1`] = `[]`; 50 | 51 | exports[`'postprocess' option should work with the "postprocessor" option: errors 1`] = `[]`; 52 | 53 | exports[`'postprocess' option should work with the "postprocessor" option: module 1`] = ` 54 | "// Imports 55 | var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png", import.meta.url); 56 | // Module 57 | var code = \` 58 | 59 | 60 | 61 |
\${ require('./gallery.html').default }
62 | 63 | \`; 64 | // Exports 65 | export default code;" 66 | `; 67 | 68 | exports[`'postprocess' option should work with the "postprocessor" option: result 1`] = ` 69 | " 70 | 71 | 72 | 73 |

Gallery

74 | 75 | " 76 | `; 77 | 78 | exports[`'postprocess' option should work with the "postprocessor" option: warnings 1`] = `[]`; 79 | -------------------------------------------------------------------------------- /test/fixtures/webpackIgnore.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Elva dressed as a fairy 18 | 19 | Elva dressed as a fairy 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Flowers 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | 102 | 103 | 104 |
105 | 106 | 107 | 108 |
109 | 110 | 111 |
112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /test/postprocessor-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import { 4 | compile, 5 | execute, 6 | getCompiler, 7 | getErrors, 8 | getModuleSource, 9 | getWarnings, 10 | readAsset, 11 | } from "./helpers"; 12 | 13 | describe("'postprocess' option", () => { 14 | it('should work with the "postprocessor" option', async () => { 15 | const compiler = getCompiler("postprocessor.html", { 16 | postprocessor: (content, loaderContext) => { 17 | expect(typeof content).toBe("string"); 18 | expect(loaderContext).toBeDefined(); 19 | 20 | const isTemplateLiteralSupported = content[0] === "`"; 21 | 22 | return content 23 | .replaceAll("<%=", isTemplateLiteralSupported ? "${" : '" +') 24 | .replaceAll("%>", isTemplateLiteralSupported ? "}" : '+ "'); 25 | }, 26 | }); 27 | const stats = await compile(compiler); 28 | 29 | expect(getModuleSource("./postprocessor.html", stats)).toMatchSnapshot( 30 | "module", 31 | ); 32 | expect( 33 | execute(readAsset("main.bundle.js", compiler, stats)), 34 | ).toMatchSnapshot("result"); 35 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 36 | expect(getErrors(stats)).toMatchSnapshot("errors"); 37 | }); 38 | 39 | it('should work with the "postprocessor" option #1', async () => { 40 | const compiler = getCompiler( 41 | "postprocessor.html", 42 | { 43 | postprocessor: (content, loaderContext) => { 44 | expect(typeof content).toBe("string"); 45 | expect(loaderContext).toBeDefined(); 46 | 47 | const isTemplateLiteralSupported = content[0] === "`"; 48 | 49 | return content 50 | .replaceAll("<%=", isTemplateLiteralSupported ? "${" : '" +') 51 | .replaceAll("%>", isTemplateLiteralSupported ? "}" : '+ "'); 52 | }, 53 | }, 54 | { 55 | output: { 56 | path: path.resolve(__dirname, "./outputs"), 57 | filename: "[name].bundle.js", 58 | chunkFilename: "[name].chunk.js", 59 | chunkLoading: "require", 60 | publicPath: "/webpack/public/path/", 61 | library: "___TEST___", 62 | assetModuleFilename: "[name][ext]", 63 | hashFunction: "xxhash64", 64 | environment: { templateLiteral: false }, 65 | }, 66 | }, 67 | ); 68 | const stats = await compile(compiler); 69 | 70 | expect(getModuleSource("./postprocessor.html", stats)).toMatchSnapshot( 71 | "module", 72 | ); 73 | expect( 74 | execute(readAsset("main.bundle.js", compiler, stats)), 75 | ).toMatchSnapshot("result"); 76 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 77 | expect(getErrors(stats)).toMatchSnapshot("errors"); 78 | }); 79 | 80 | it('should work with async "postprocessor" function option', async () => { 81 | const compiler = getCompiler("preprocessor.hbs", { 82 | postprocessor: async (content, loaderContext) => { 83 | await expect(typeof content).toBe("string"); 84 | await expect(loaderContext).toBeDefined(); 85 | 86 | const isTemplateLiteralSupported = content[0] === "`"; 87 | 88 | return content 89 | .replaceAll("<%=", isTemplateLiteralSupported ? "${" : '" +') 90 | .replaceAll("%>", isTemplateLiteralSupported ? "}" : '+ "'); 91 | }, 92 | }); 93 | const stats = await compile(compiler); 94 | 95 | expect(getModuleSource("./preprocessor.hbs", stats)).toMatchSnapshot( 96 | "module", 97 | ); 98 | expect( 99 | execute(readAsset("main.bundle.js", compiler, stats)), 100 | ).toMatchSnapshot("result"); 101 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 102 | expect(getErrors(stats)).toMatchSnapshot("errors"); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/__snapshots__/preprocessor-option.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`'preprocess' option should work with async "preprocessor" function option: errors 1`] = `[]`; 4 | 5 | exports[`'preprocess' option should work with async "preprocessor" function option: module 1`] = ` 6 | "// Imports 7 | var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png", import.meta.url); 8 | // Module 9 | var code = \`
10 |

Firstname Lastname

11 | alt 12 |
13 | \`; 14 | // Exports 15 | export default code;" 16 | `; 17 | 18 | exports[`'preprocess' option should work with async "preprocessor" function option: result 1`] = ` 19 | "
20 |

Firstname Lastname

21 | alt 22 |
23 | " 24 | `; 25 | 26 | exports[`'preprocess' option should work with async "preprocessor" function option: warnings 1`] = `[]`; 27 | 28 | exports[`'preprocess' option should work with the "preprocessor" option #2: errors 1`] = `[]`; 29 | 30 | exports[`'preprocess' option should work with the "preprocessor" option #2: module 1`] = ` 31 | "// Imports 32 | var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png.webp", import.meta.url); 33 | var ___HTML_LOADER_IMPORT_1___ = new URL("./image.png", import.meta.url); 34 | // Module 35 | var code = \` 36 | \`; 37 | // Exports 38 | export default code;" 39 | `; 40 | 41 | exports[`'preprocess' option should work with the "preprocessor" option #2: result 1`] = ` 42 | " 43 | " 44 | `; 45 | 46 | exports[`'preprocess' option should work with the "preprocessor" option #2: warnings 1`] = `[]`; 47 | 48 | exports[`'preprocess' option should work with the "preprocessor" option: errors 1`] = `[]`; 49 | 50 | exports[`'preprocess' option should work with the "preprocessor" option: module 1`] = ` 51 | "// Imports 52 | var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png", import.meta.url); 53 | // Module 54 | var code = \`
55 |

Firstname Lastname

56 | alt 57 |
58 | \`; 59 | // Exports 60 | export default code;" 61 | `; 62 | 63 | exports[`'preprocess' option should work with the "preprocessor" option: result 1`] = ` 64 | "
65 |

Firstname Lastname

66 | alt 67 |
68 | " 69 | `; 70 | 71 | exports[`'preprocess' option should work with the "preprocessor" option: warnings 1`] = `[]`; 72 | 73 | exports[`'preprocess' option should work with the async "preprocessor" function option #2: errors 1`] = `[]`; 74 | 75 | exports[`'preprocess' option should work with the async "preprocessor" function option #2: module 1`] = ` 76 | "// Imports 77 | var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png.webp", import.meta.url); 78 | var ___HTML_LOADER_IMPORT_1___ = new URL("./image.png", import.meta.url); 79 | // Module 80 | var code = \` 81 | \`; 82 | // Exports 83 | export default code;" 84 | `; 85 | 86 | exports[`'preprocess' option should work with the async "preprocessor" function option #2: result 1`] = ` 87 | " 88 | " 89 | `; 90 | 91 | exports[`'preprocess' option should work with the async "preprocessor" function option #2: warnings 1`] = `[]`; 92 | -------------------------------------------------------------------------------- /test/preprocessor-option.test.js: -------------------------------------------------------------------------------- 1 | import Handlebars from "handlebars"; 2 | import posthtml from "posthtml"; 3 | import posthtmlWebp from "posthtml-webp"; 4 | 5 | import { 6 | compile, 7 | execute, 8 | getCompiler, 9 | getErrors, 10 | getModuleSource, 11 | getWarnings, 12 | readAsset, 13 | } from "./helpers"; 14 | 15 | describe("'preprocess' option", () => { 16 | it('should work with the "preprocessor" option', async () => { 17 | const compiler = getCompiler("preprocessor.hbs", { 18 | preprocessor: (content, loaderContext) => { 19 | expect(typeof content).toBe("string"); 20 | expect(loaderContext).toBeDefined(); 21 | 22 | let result; 23 | 24 | try { 25 | result = Handlebars.compile(content)({ 26 | firstname: "Firstname", 27 | lastname: "Lastname", 28 | }); 29 | } catch (error) { 30 | loaderContext.emitError(error); 31 | 32 | return content; 33 | } 34 | 35 | return result; 36 | }, 37 | }); 38 | const stats = await compile(compiler); 39 | 40 | expect(getModuleSource("./preprocessor.hbs", stats)).toMatchSnapshot( 41 | "module", 42 | ); 43 | expect( 44 | execute(readAsset("main.bundle.js", compiler, stats)), 45 | ).toMatchSnapshot("result"); 46 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 47 | expect(getErrors(stats)).toMatchSnapshot("errors"); 48 | }); 49 | 50 | it('should work with async "preprocessor" function option', async () => { 51 | const compiler = getCompiler("preprocessor.hbs", { 52 | preprocessor: async (content, loaderContext) => { 53 | await expect(typeof content).toBe("string"); 54 | await expect(loaderContext).toBeDefined(); 55 | 56 | let result; 57 | 58 | try { 59 | result = await Handlebars.compile(content)({ 60 | firstname: "Firstname", 61 | lastname: "Lastname", 62 | }); 63 | } catch (error) { 64 | await loaderContext.emitError(error); 65 | 66 | return content; 67 | } 68 | 69 | return result; 70 | }, 71 | }); 72 | const stats = await compile(compiler); 73 | 74 | expect(getModuleSource("./preprocessor.hbs", stats)).toMatchSnapshot( 75 | "module", 76 | ); 77 | expect( 78 | execute(readAsset("main.bundle.js", compiler, stats)), 79 | ).toMatchSnapshot("result"); 80 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 81 | expect(getErrors(stats)).toMatchSnapshot("errors"); 82 | }); 83 | 84 | it('should work with the "preprocessor" option #2', async () => { 85 | const plugin = posthtmlWebp(); 86 | const compiler = getCompiler("posthtml.html", { 87 | preprocessor: (content, loaderContext) => { 88 | expect(typeof content).toBe("string"); 89 | expect(loaderContext).toBeDefined(); 90 | 91 | let result; 92 | 93 | try { 94 | result = posthtml().use(plugin).process(content, { sync: true }); 95 | } catch (error) { 96 | loaderContext.emitError(error); 97 | 98 | return content; 99 | } 100 | 101 | return result.html; 102 | }, 103 | }); 104 | const stats = await compile(compiler); 105 | 106 | expect(getModuleSource("./posthtml.html", stats)).toMatchSnapshot("module"); 107 | expect( 108 | execute(readAsset("main.bundle.js", compiler, stats)), 109 | ).toMatchSnapshot("result"); 110 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 111 | expect(getErrors(stats)).toMatchSnapshot("errors"); 112 | }); 113 | 114 | it('should work with the async "preprocessor" function option #2', async () => { 115 | const plugin = posthtmlWebp(); 116 | const compiler = getCompiler("posthtml.html", { 117 | preprocessor: async (content, loaderContext) => { 118 | await expect(typeof content).toBe("string"); 119 | await expect(loaderContext).toBeDefined(); 120 | 121 | let result; 122 | 123 | try { 124 | result = await posthtml() 125 | .use(plugin) 126 | .process(content, { sync: true }); 127 | } catch (error) { 128 | loaderContext.emitError(error); 129 | 130 | return content; 131 | } 132 | 133 | return result.html; 134 | }, 135 | }); 136 | const stats = await compile(compiler); 137 | 138 | expect(getModuleSource("./posthtml.html", stats)).toMatchSnapshot("module"); 139 | expect( 140 | execute(readAsset("main.bundle.js", compiler, stats)), 141 | ).toMatchSnapshot("result"); 142 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 143 | expect(getErrors(stats)).toMatchSnapshot("errors"); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/minimize-option.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import { 6 | compile, 7 | execute, 8 | getCompiler, 9 | getErrors, 10 | getModuleSource, 11 | getWarnings, 12 | readAsset, 13 | } from "./helpers"; 14 | 15 | describe('"minimize" option', () => { 16 | it("should be turned off by default", async () => { 17 | const compiler = getCompiler("simple.js"); 18 | const stats = await compile(compiler); 19 | 20 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 21 | expect( 22 | execute(readAsset("main.bundle.js", compiler, stats)), 23 | ).toMatchSnapshot("result"); 24 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 25 | expect(getErrors(stats)).toMatchSnapshot("errors"); 26 | }); 27 | 28 | it('should be turned off in "development" mode', async () => { 29 | const compiler = getCompiler("simple.js", {}, { mode: "development" }); 30 | const stats = await compile(compiler); 31 | 32 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 33 | expect( 34 | execute(readAsset("main.bundle.js", compiler, stats)), 35 | ).toMatchSnapshot("result"); 36 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 37 | expect(getErrors(stats)).toMatchSnapshot("errors"); 38 | }); 39 | 40 | it('should be turned on in "production" mode', async () => { 41 | const compiler = getCompiler("simple.js", {}, { mode: "production" }); 42 | const stats = await compile(compiler); 43 | 44 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 45 | expect( 46 | execute(readAsset("main.bundle.js", compiler, stats)), 47 | ).toMatchSnapshot("result"); 48 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 49 | expect(getErrors(stats)).toMatchSnapshot("errors"); 50 | }); 51 | 52 | it('should not work with a value equal to "false"', async () => { 53 | const compiler = getCompiler("simple.js", { minimize: false }); 54 | const stats = await compile(compiler); 55 | 56 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 57 | expect( 58 | execute(readAsset("main.bundle.js", compiler, stats)), 59 | ).toMatchSnapshot("result"); 60 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 61 | expect(getErrors(stats)).toMatchSnapshot("errors"); 62 | }); 63 | 64 | it('should work with a value equal to "true"', async () => { 65 | const compiler = getCompiler("simple.js", { minimize: true }); 66 | const stats = await compile(compiler); 67 | 68 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 69 | expect( 70 | execute(readAsset("main.bundle.js", compiler, stats)), 71 | ).toMatchSnapshot("result"); 72 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 73 | expect(getErrors(stats)).toMatchSnapshot("errors"); 74 | }); 75 | 76 | it("should support options for minimizer", async () => { 77 | const compiler = getCompiler("simple.js", { 78 | minimize: { 79 | collapseWhitespace: true, 80 | conservativeCollapse: true, 81 | removeAttributeQuotes: true, 82 | keepClosingSlash: true, 83 | minifyJS: true, 84 | minifyCSS: true, 85 | removeScriptTypeAttributes: true, 86 | removeStyleLinkTypeAttributes: true, 87 | useShortDoctype: true, 88 | removeComments: false, 89 | }, 90 | }); 91 | const stats = await compile(compiler); 92 | 93 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 94 | expect( 95 | execute(readAsset("main.bundle.js", compiler, stats)), 96 | ).toMatchSnapshot("result"); 97 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 98 | expect(getErrors(stats)).toMatchSnapshot("errors"); 99 | }); 100 | 101 | it("should emit an error on broken HTML syntax", async () => { 102 | const compiler = getCompiler("broken-html-syntax.js", { minimize: true }); 103 | const stats = await compile(compiler); 104 | 105 | expect(getModuleSource("./broken-html-syntax.html", stats)).toMatchSnapshot( 106 | "module", 107 | ); 108 | expect( 109 | execute(readAsset("main.bundle.js", compiler, stats)), 110 | ).toMatchSnapshot("result"); 111 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 112 | expect(getErrors(stats)).toMatchSnapshot("errors"); 113 | }); 114 | 115 | it("should work with XHTML", async () => { 116 | const compiler = getCompiler("XHTML.js", { minimize: true }); 117 | const stats = await compile(compiler); 118 | 119 | expect(getModuleSource("./XHTML.html", stats)).toMatchSnapshot("module"); 120 | expect( 121 | execute(readAsset("main.bundle.js", compiler, stats)), 122 | ).toMatchSnapshot("result"); 123 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 124 | expect(getErrors(stats)).toMatchSnapshot("errors"); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/validate-options.test.js: -------------------------------------------------------------------------------- 1 | import { compile, getCompiler } from "./helpers"; 2 | 3 | describe("validate options", () => { 4 | const tests = { 5 | sources: { 6 | success: [ 7 | true, 8 | false, 9 | { 10 | list: [ 11 | { 12 | attribute: "src", 13 | type: "src", 14 | }, 15 | ], 16 | }, 17 | { 18 | list: [ 19 | { 20 | tag: "img", 21 | attribute: "src", 22 | type: "src", 23 | }, 24 | ], 25 | }, 26 | { 27 | list: [ 28 | { 29 | tag: "img", 30 | attribute: "src", 31 | type: "src", 32 | filter: () => true, 33 | }, 34 | ], 35 | }, 36 | { 37 | list: [ 38 | { 39 | tag: "img", 40 | attribute: "src", 41 | type: "src", 42 | }, 43 | { 44 | tag: "img", 45 | attribute: "srcset", 46 | type: "srcset", 47 | }, 48 | ], 49 | }, 50 | { 51 | list: [ 52 | "...", 53 | { 54 | tag: "img", 55 | attribute: "srcset", 56 | type: "srcset", 57 | }, 58 | ], 59 | }, 60 | { urlFilter: () => true }, 61 | { scriptingEnabled: true }, 62 | { scriptingEnabled: false }, 63 | { 64 | list: [ 65 | { 66 | tag: "img", 67 | attribute: "src", 68 | type: "src", 69 | }, 70 | { 71 | tag: "img", 72 | attribute: "srcset", 73 | type: "srcset", 74 | }, 75 | ], 76 | urlFilter: () => true, 77 | }, 78 | ], 79 | failure: [ 80 | "true", 81 | [], 82 | { 83 | list: [], 84 | }, 85 | { 86 | list: [ 87 | { 88 | tag: "img", 89 | attribute: "src", 90 | }, 91 | ], 92 | }, 93 | { 94 | list: [ 95 | { 96 | tag: "", 97 | attribute: "src", 98 | type: "src", 99 | }, 100 | ], 101 | }, 102 | { 103 | list: [ 104 | { 105 | tag: "img", 106 | attribute: "src", 107 | type: "src", 108 | }, 109 | { 110 | tag: "img", 111 | attribute: "src", 112 | type: "src", 113 | }, 114 | ], 115 | }, 116 | { 117 | list: [ 118 | { 119 | tag: "img", 120 | attribute: "src", 121 | type: "src", 122 | filter: "test", 123 | }, 124 | ], 125 | }, 126 | { urlFilter: false }, 127 | { scriptingEnabled: "true" }, 128 | { unknown: true }, 129 | ], 130 | }, 131 | esModule: { 132 | success: [true, false], 133 | failure: ["true"], 134 | }, 135 | minimize: { 136 | success: [true, false, {}], 137 | failure: ["true"], 138 | }, 139 | preprocessor: { 140 | success: [() => []], 141 | failure: ["true"], 142 | }, 143 | unknown: { 144 | success: [], 145 | failure: [1, true, false, "test", /test/, [], {}, { foo: "bar" }], 146 | }, 147 | }; 148 | 149 | function stringifyValue(value) { 150 | if ( 151 | Array.isArray(value) || 152 | (value && typeof value === "object" && value.constructor === Object) 153 | ) { 154 | return JSON.stringify(value); 155 | } 156 | 157 | return value; 158 | } 159 | 160 | async function createTestCase(key, value, type) { 161 | it(`should ${ 162 | type === "success" ? "successfully validate" : "throw an error on" 163 | } the "${key}" option with "${stringifyValue(value)}" value`, async () => { 164 | // For loaders 165 | const compiler = getCompiler("simple.js", { [key]: value }); 166 | 167 | let stats; 168 | 169 | try { 170 | stats = await compile(compiler); 171 | } finally { 172 | if (type === "success") { 173 | expect(stats.hasErrors()).toBe(false); 174 | } else if (type === "failure") { 175 | const { 176 | compilation: { errors }, 177 | } = stats; 178 | 179 | expect(errors).toHaveLength(1); 180 | expect(() => { 181 | throw new Error(errors[0].error.message); 182 | }).toThrowErrorMatchingSnapshot(); 183 | } 184 | } 185 | }); 186 | } 187 | 188 | for (const [key, values] of Object.entries(tests)) { 189 | for (const type of Object.keys(values)) { 190 | for (const value of values[type]) { 191 | createTestCase(key, value, type); 192 | } 193 | } 194 | } 195 | }); 196 | -------------------------------------------------------------------------------- /src/plugins/sources-plugin.js: -------------------------------------------------------------------------------- 1 | import { parse } from "parse5"; 2 | 3 | import { 4 | getFilter, 5 | requestify, 6 | traverse, 7 | webpackIgnoreCommentRegexp, 8 | } from "../utils"; 9 | 10 | const DOUBLE_QUOTE = '"'.charCodeAt(0); 11 | const SINGLE_QUOTE = "'".charCodeAt(0); 12 | 13 | export default (options) => 14 | function process(html) { 15 | const sources = []; 16 | const document = parse(html, { 17 | sourceCodeLocationInfo: true, 18 | scriptingEnabled: options.sources.scriptingEnabled, 19 | }); 20 | 21 | let needIgnore = false; 22 | 23 | traverse(document, (node) => { 24 | const { tagName, attrs: attributes, sourceCodeLocation } = node; 25 | 26 | if (node.nodeName === "#comment") { 27 | const match = node.data.match(webpackIgnoreCommentRegexp); 28 | 29 | if (match) { 30 | needIgnore = match[2] === "true"; 31 | } 32 | 33 | return; 34 | } 35 | 36 | if (!tagName) { 37 | return; 38 | } 39 | 40 | if (needIgnore) { 41 | needIgnore = false; 42 | return; 43 | } 44 | 45 | for (const attribute of attributes) { 46 | let { name } = attribute; 47 | 48 | name = attribute.prefix ? `${attribute.prefix}:${name}` : name; 49 | 50 | const handlers = new Map([ 51 | ...(options.sources.list.get("*") || new Map()), 52 | ...(options.sources.list.get(tagName.toLowerCase()) || new Map()), 53 | ]); 54 | 55 | if (handlers.size === 0) { 56 | continue; 57 | } 58 | 59 | const handler = handlers.get(name.toLowerCase()); 60 | 61 | if (!handler) { 62 | continue; 63 | } 64 | 65 | if ( 66 | handler.filter && 67 | !handler.filter(tagName, name, attributes, options.resourcePath) 68 | ) { 69 | continue; 70 | } 71 | 72 | const attributeAndValue = html.slice( 73 | sourceCodeLocation.attrs[name].startOffset, 74 | sourceCodeLocation.attrs[name].endOffset, 75 | ); 76 | const isValueQuoted = 77 | attributeAndValue.charCodeAt(attributeAndValue.length - 1) === 78 | DOUBLE_QUOTE || 79 | attributeAndValue.charCodeAt(attributeAndValue.length - 1) === 80 | SINGLE_QUOTE; 81 | const valueStartOffset = 82 | sourceCodeLocation.attrs[name].startOffset + 83 | attributeAndValue.indexOf(attribute.value); 84 | const valueEndOffset = 85 | sourceCodeLocation.attrs[name].endOffset - (isValueQuoted ? 1 : 0); 86 | const optionsForTypeFn = { 87 | tag: tagName, 88 | startTag: { 89 | startOffset: sourceCodeLocation.startTag.startOffset, 90 | endOffset: sourceCodeLocation.startTag.endOffset, 91 | }, 92 | endTag: sourceCodeLocation.endTag 93 | ? { 94 | startOffset: sourceCodeLocation.endTag.startOffset, 95 | endOffset: sourceCodeLocation.endTag.endOffset, 96 | } 97 | : undefined, 98 | attributes, 99 | attribute: name, 100 | attributePrefix: attribute.prefix, 101 | attributeNamespace: attribute.namespace, 102 | attributeStartOffset: sourceCodeLocation.attrs[name].startOffset, 103 | attributeEndOffset: sourceCodeLocation.attrs[name].endOffset, 104 | value: attribute.value, 105 | isSupportAbsoluteURL: options.isSupportAbsoluteURL, 106 | isSupportDataURL: options.isSupportDataURL, 107 | isValueQuoted, 108 | valueEndOffset, 109 | valueStartOffset, 110 | html, 111 | }; 112 | 113 | let result; 114 | 115 | try { 116 | result = handler.type(optionsForTypeFn); 117 | } catch (error) { 118 | options.errors.push(error); 119 | } 120 | 121 | result = Array.isArray(result) ? result : [result]; 122 | 123 | for (const source of result) { 124 | if (!source) { 125 | continue; 126 | } 127 | 128 | sources.push({ ...source, name, isValueQuoted }); 129 | } 130 | } 131 | }); 132 | 133 | const urlFilter = getFilter(options.sources.urlFilter); 134 | const imports = new Map(); 135 | const replacements = new Map(); 136 | 137 | let offset = 0; 138 | 139 | for (const source of sources) { 140 | const { name, value, isValueQuoted, startOffset, endOffset } = source; 141 | 142 | let request = value; 143 | 144 | if (!urlFilter(name, value, options.resourcePath)) { 145 | continue; 146 | } 147 | 148 | let hash; 149 | const indexHash = request.lastIndexOf("#"); 150 | 151 | if (indexHash >= 0) { 152 | hash = request.slice(Math.max(0, indexHash)); 153 | request = request.slice(0, Math.max(0, indexHash)); 154 | } 155 | 156 | request = requestify(options.context, request); 157 | 158 | let importName = imports.get(request); 159 | 160 | if (!importName) { 161 | importName = `___HTML_LOADER_IMPORT_${imports.size}___`; 162 | imports.set(request, importName); 163 | 164 | options.imports.push({ importName, request }); 165 | } 166 | 167 | const replacementKey = JSON.stringify({ request, isValueQuoted, hash }); 168 | let replacementName = replacements.get(replacementKey); 169 | 170 | if (!replacementName) { 171 | replacementName = `___HTML_LOADER_REPLACEMENT_${replacements.size}___`; 172 | replacements.set(replacementKey, replacementName); 173 | 174 | options.replacements.push({ 175 | replacementName, 176 | importName, 177 | hash, 178 | isValueQuoted, 179 | }); 180 | } 181 | 182 | html = 183 | html.slice(0, startOffset + offset) + 184 | replacementName + 185 | html.slice(endOffset + offset); 186 | 187 | offset += startOffset + replacementName.length - endOffset; 188 | } 189 | 190 | return html; 191 | }; 192 | -------------------------------------------------------------------------------- /test/loader.test.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | import HtmlWebpackPlugin from "html-webpack-plugin"; 5 | 6 | import { 7 | compile, 8 | execute, 9 | getCompiler, 10 | getErrors, 11 | getModuleSource, 12 | getWarnings, 13 | readAsset, 14 | } from "./helpers"; 15 | 16 | describe("loader", () => { 17 | it("should work", async () => { 18 | const compiler = getCompiler("simple.js"); 19 | const stats = await compile(compiler); 20 | 21 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 22 | expect( 23 | execute(readAsset("main.bundle.js", compiler, stats)), 24 | ).toMatchSnapshot("result"); 25 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 26 | expect(getErrors(stats)).toMatchSnapshot("errors"); 27 | }); 28 | 29 | it("should work with an empty file", async () => { 30 | const compiler = getCompiler("empty.js"); 31 | const stats = await compile(compiler); 32 | 33 | expect(getModuleSource("./empty.html", stats)).toMatchSnapshot("module"); 34 | expect( 35 | execute(readAsset("main.bundle.js", compiler, stats)), 36 | ).toMatchSnapshot("result"); 37 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 38 | expect(getErrors(stats)).toMatchSnapshot("errors"); 39 | }); 40 | 41 | it("should not make bad things with templates", async () => { 42 | const compiler = getCompiler("template.js"); 43 | const stats = await compile(compiler); 44 | 45 | expect(getModuleSource("./template.html", stats)).toMatchSnapshot("module"); 46 | expect( 47 | execute(readAsset("main.bundle.js", compiler, stats)), 48 | ).toMatchSnapshot("result"); 49 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 50 | expect(getErrors(stats)).toMatchSnapshot("errors"); 51 | }); 52 | 53 | it("should not failed contain invisible spaces", async () => { 54 | const source = fs.readFileSync( 55 | path.resolve(__dirname, "./fixtures/invisible-space.html"), 56 | ); 57 | 58 | expect(/[\u2028\u2029]/.test(source)).toBe(true); 59 | 60 | const compiler = getCompiler("invisible-space.js"); 61 | const stats = await compile(compiler); 62 | 63 | const moduleSource = getModuleSource("./invisible-space.html", stats); 64 | 65 | expect(moduleSource).toMatchSnapshot("module"); 66 | expect(/[\u2028\u2029]/.test(moduleSource)).toBe(false); 67 | expect( 68 | execute(readAsset("main.bundle.js", compiler, stats)), 69 | ).toMatchSnapshot("result"); 70 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 71 | expect(getErrors(stats)).toMatchSnapshot("errors"); 72 | }); 73 | 74 | it("should emit an error on broken HTML syntax", async () => { 75 | const compiler = getCompiler("broken-html-syntax.js"); 76 | const stats = await compile(compiler); 77 | 78 | expect(getModuleSource("./broken-html-syntax.html", stats)).toMatchSnapshot( 79 | "module", 80 | ); 81 | expect( 82 | execute(readAsset("main.bundle.js", compiler, stats)), 83 | ).toMatchSnapshot("result"); 84 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 85 | expect(getErrors(stats)).toMatchSnapshot("errors"); 86 | }); 87 | 88 | it("should work with server-relative url", async () => { 89 | const compiler = getCompiler("nested.js"); 90 | const stats = await compile(compiler); 91 | 92 | expect(getModuleSource("./nested.html", stats)).toMatchSnapshot("module"); 93 | expect( 94 | execute(readAsset("main.bundle.js", compiler, stats)), 95 | ).toMatchSnapshot("result"); 96 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 97 | expect(getErrors(stats)).toMatchSnapshot("errors"); 98 | }); 99 | 100 | it('should work with "resolve.roots"', async () => { 101 | const compiler = getCompiler( 102 | "roots.js", 103 | {}, 104 | { 105 | resolve: { 106 | roots: [path.resolve(__dirname, "fixtures/roots")], 107 | }, 108 | }, 109 | ); 110 | const stats = await compile(compiler); 111 | 112 | expect(getModuleSource("./roots.html", stats)).toMatchSnapshot("module"); 113 | expect( 114 | execute(readAsset("main.bundle.js", compiler, stats)), 115 | ).toMatchSnapshot("result"); 116 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 117 | expect(getErrors(stats)).toMatchSnapshot("errors"); 118 | }); 119 | 120 | it("should work with file protocol", async () => { 121 | const file = path.resolve(__dirname, "fixtures", "generated-2.html"); 122 | const absolutePath = path.resolve(__dirname, "fixtures", "image.png"); 123 | 124 | fs.writeFileSync(file, ``); 125 | 126 | const compiler = getCompiler("file-protocol.js"); 127 | const stats = await compile(compiler); 128 | 129 | // expect(getModuleSource('./generated-2.html', stats)).toMatchSnapshot( 130 | // 'module' 131 | // ); 132 | expect( 133 | execute(readAsset("main.bundle.js", compiler, stats)), 134 | ).toMatchSnapshot("result"); 135 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 136 | expect(getErrors(stats)).toMatchSnapshot("errors"); 137 | }); 138 | 139 | it("should pass queries to other loader", async () => { 140 | const compiler = getCompiler( 141 | "other-loader-query.js", 142 | {}, 143 | { 144 | module: { 145 | rules: [ 146 | { 147 | test: /\.svg$/i, 148 | resourceQuery: /color/, 149 | enforce: "pre", 150 | use: { 151 | loader: path.resolve( 152 | __dirname, 153 | "./helpers/svg-color-loader.js", 154 | ), 155 | }, 156 | }, 157 | { 158 | test: /\.html$/i, 159 | rules: [{ loader: path.resolve(__dirname, "../src") }], 160 | }, 161 | ], 162 | }, 163 | }, 164 | ); 165 | const stats = await compile(compiler); 166 | 167 | expect(getModuleSource("./other-loader-query.html", stats)).toMatchSnapshot( 168 | "module", 169 | ); 170 | expect( 171 | execute(readAsset("main.bundle.js", compiler, stats)), 172 | ).toMatchSnapshot("result"); 173 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 174 | expect(getErrors(stats)).toMatchSnapshot("errors"); 175 | }); 176 | 177 | it("should work with webpackIgnore comment", async () => { 178 | const compiler = getCompiler("webpackIgnore.js"); 179 | const stats = await compile(compiler); 180 | 181 | expect(getModuleSource("./webpackIgnore.html", stats)).toMatchSnapshot( 182 | "module", 183 | ); 184 | expect( 185 | execute(readAsset("main.bundle.js", compiler, stats)), 186 | ).toMatchSnapshot("result"); 187 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 188 | expect(getErrors(stats)).toMatchSnapshot("errors"); 189 | }); 190 | 191 | it('should work with "html-webpack-plugin" plugin', async () => { 192 | const compiler = getCompiler( 193 | "entry.js", 194 | {}, 195 | { 196 | output: { 197 | publicPath: "http://example.com", 198 | hashFunction: "xxhash64", 199 | }, 200 | module: { 201 | rules: [ 202 | { 203 | test: /\.html$/i, 204 | loader: path.resolve(__dirname, "../src"), 205 | }, 206 | ], 207 | }, 208 | plugins: [ 209 | new HtmlWebpackPlugin({ 210 | template: path.resolve(__dirname, "fixtures/template-html.html"), 211 | }), 212 | ], 213 | }, 214 | ); 215 | const stats = await compile(compiler); 216 | 217 | expect(readAsset("index.html", compiler, stats)).toMatchSnapshot("result"); 218 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 219 | expect(getErrors(stats)).toMatchSnapshot("errors"); 220 | }); 221 | 222 | it("should work with `experiments.buildHttp`", async () => { 223 | const compiler = getCompiler( 224 | "simple.js", 225 | {}, 226 | { 227 | experiments: { 228 | buildHttp: { 229 | allowedUris: [() => true], 230 | lockfileLocation: path.resolve( 231 | __dirname, 232 | "./outputs/url/lock.json", 233 | ), 234 | cacheLocation: path.resolve(__dirname, "./outputs/url"), 235 | }, 236 | }, 237 | }, 238 | ); 239 | const stats = await compile(compiler); 240 | 241 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 242 | expect( 243 | execute(readAsset("main.bundle.js", compiler, stats)), 244 | ).toMatchSnapshot("result"); 245 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 246 | expect(getErrors(stats)).toMatchSnapshot("errors"); 247 | }); 248 | }); 249 | -------------------------------------------------------------------------------- /test/__snapshots__/validate-options.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`validate options should throw an error on the "esModule" option with "true" value 1`] = ` 4 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 5 | - options.esModule should be a boolean. 6 | -> Enable or disable ES modules syntax. 7 | -> Read more at https://github.com/webpack/html-loader#esmodule" 8 | `; 9 | 10 | exports[`validate options should throw an error on the "minimize" option with "true" value 1`] = ` 11 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 12 | - options.minimize should be one of these: 13 | boolean | object { … } 14 | -> Tell html-loader to minimize HTML. 15 | -> Read more at https://github.com/webpack/html-loader#minimize 16 | Details: 17 | * options.minimize should be a boolean. 18 | * options.minimize should be an object: 19 | object { … }" 20 | `; 21 | 22 | exports[`validate options should throw an error on the "preprocessor" option with "true" value 1`] = ` 23 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 24 | - options.preprocessor should be an instance of function. 25 | -> Allows pre-processing of content before handling. 26 | -> Read more at https://github.com/webpack/html-loader#preprocessor" 27 | `; 28 | 29 | exports[`validate options should throw an error on the "sources" option with "[]" value 1`] = ` 30 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 31 | - options.sources should be one of these: 32 | boolean | object { list?, urlFilter?, scriptingEnabled? } 33 | -> By default every loadable attributes (for example - ) is imported (const img = require('./image.png'). You may need to specify loaders for images in your configuration. 34 | -> Read more at https://github.com/webpack/html-loader#sources 35 | Details: 36 | * options.sources should be a boolean. 37 | * options.sources should be an object: 38 | object { list?, urlFilter?, scriptingEnabled? }" 39 | `; 40 | 41 | exports[`validate options should throw an error on the "sources" option with "{"list":[]}" value 1`] = ` 42 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 43 | - options.sources.list should be a non-empty array." 44 | `; 45 | 46 | exports[`validate options should throw an error on the "sources" option with "{"list":[{"tag":"","attribute":"src","type":"src"}]}" value 1`] = ` 47 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 48 | - options.sources.list[0].tag should be a non-empty string." 49 | `; 50 | 51 | exports[`validate options should throw an error on the "sources" option with "{"list":[{"tag":"img","attribute":"src","type":"src","filter":"test"}]}" value 1`] = ` 52 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 53 | - options.sources.list[0].filter should be an instance of function." 54 | `; 55 | 56 | exports[`validate options should throw an error on the "sources" option with "{"list":[{"tag":"img","attribute":"src","type":"src"},{"tag":"img","attribute":"src","type":"src"}]}" value 1`] = ` 57 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 58 | - options.sources.list should not contain the item '[object Object]' twice." 59 | `; 60 | 61 | exports[`validate options should throw an error on the "sources" option with "{"list":[{"tag":"img","attribute":"src"}]}" value 1`] = ` 62 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 63 | - options.sources.list[0] misses the property 'type'. Should be: 64 | "src" | "srcset"" 65 | `; 66 | 67 | exports[`validate options should throw an error on the "sources" option with "{"scriptingEnabled":"true"}" value 1`] = ` 68 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 69 | - options.sources.scriptingEnabled should be a boolean." 70 | `; 71 | 72 | exports[`validate options should throw an error on the "sources" option with "{"unknown":true}" value 1`] = ` 73 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 74 | - options.sources has an unknown property 'unknown'. These properties are valid: 75 | object { list?, urlFilter?, scriptingEnabled? }" 76 | `; 77 | 78 | exports[`validate options should throw an error on the "sources" option with "{"urlFilter":false}" value 1`] = ` 79 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 80 | - options.sources.urlFilter should be an instance of function." 81 | `; 82 | 83 | exports[`validate options should throw an error on the "sources" option with "true" value 1`] = ` 84 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 85 | - options.sources should be one of these: 86 | boolean | object { list?, urlFilter?, scriptingEnabled? } 87 | -> By default every loadable attributes (for example - ) is imported (const img = require('./image.png'). You may need to specify loaders for images in your configuration. 88 | -> Read more at https://github.com/webpack/html-loader#sources 89 | Details: 90 | * options.sources should be a boolean. 91 | * options.sources should be an object: 92 | object { list?, urlFilter?, scriptingEnabled? }" 93 | `; 94 | 95 | exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` 96 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 97 | - options has an unknown property 'unknown'. These properties are valid: 98 | object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" 99 | `; 100 | 101 | exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = ` 102 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 103 | - options has an unknown property 'unknown'. These properties are valid: 104 | object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" 105 | `; 106 | 107 | exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = ` 108 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 109 | - options has an unknown property 'unknown'. These properties are valid: 110 | object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" 111 | `; 112 | 113 | exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = ` 114 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 115 | - options has an unknown property 'unknown'. These properties are valid: 116 | object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" 117 | `; 118 | 119 | exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = ` 120 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 121 | - options has an unknown property 'unknown'. These properties are valid: 122 | object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" 123 | `; 124 | 125 | exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = ` 126 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 127 | - options has an unknown property 'unknown'. These properties are valid: 128 | object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" 129 | `; 130 | 131 | exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = ` 132 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 133 | - options has an unknown property 'unknown'. These properties are valid: 134 | object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" 135 | `; 136 | 137 | exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = ` 138 | "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. 139 | - options has an unknown property 'unknown'. These properties are valid: 140 | object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" 141 | `; 142 | -------------------------------------------------------------------------------- /test/fixtures/sources.html: -------------------------------------------------------------------------------- 1 | Elva dressed as a fairy 2 | Elva dressed as a fairy 3 | 4 | 5 | 6 | 7 | Elva dressed as a fairy 8 | Elva dressed as a fairy 9 | Elva dressed as a fairy 10 | 11 | Elva dressed as a fairy 12 | 14 | Elva dressed as a fairy 17 | 22 | 24 | 25 | Elva dressed as a fairy 26 | Elva dressed as a fairy 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | Elva dressed as a fairy 265 | Elva dressed as a fairy 266 | Elva dressed as a fairy 267 | Elva dressed as a fairy 268 | Elva dressed as a fairy 269 | Elva dressed as a fairy 270 | Elva dressed as a fairy 271 | Elva dressed as a fairy 272 | Elva dressed as a fairy 273 | Elva dressed as a fairy 274 | Elva dressed as a fairy 275 | 276 | 277 | 278 | 279 | 280 | 283 | 284 | 287 | 288 | 291 | 292 | vincetanan@gmail.com 293 | vince@gmail.com 294 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 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 | 5 | ## [5.1.0](https://github.com/webpack-contrib/html-loader/compare/v5.0.0...v5.1.0) (2024-07-25) 6 | 7 | 8 | ### Features 9 | 10 | * added the `postprocessor` option ([#518](https://github.com/webpack-contrib/html-loader/issues/518)) ([536a204](https://github.com/webpack-contrib/html-loader/commit/536a204696c655b500c4805db8e857ac28aab7ed)) 11 | * reduce runtime code ([61f9a69](https://github.com/webpack-contrib/html-loader/commit/61f9a69a60196abdc85afaa8658faf7736cf08a9)) 12 | * support absolute URLs and DataURI ([#519](https://github.com/webpack-contrib/html-loader/issues/519)) ([cc34b06](https://github.com/webpack-contrib/html-loader/commit/cc34b068accee66d2f8ef10fb03ed90b866395db)) 13 | * using template literals in code when it supported ([#520](https://github.com/webpack-contrib/html-loader/issues/520)) ([6fa80d5](https://github.com/webpack-contrib/html-loader/commit/6fa80d51fed22613c39ffb240a0e875c9151b5b6)) 14 | 15 | ## [5.0.0](https://github.com/webpack-contrib/html-loader/compare/v4.2.0...v5.0.0) (2024-01-16) 16 | 17 | 18 | ### ⚠ BREAKING CHANGES 19 | 20 | * minimum supported Node.js version is `18.12.0` ([#504](https://github.com/webpack-contrib/html-loader/issues/504)) ([c82cfea](https://github.com/webpack-contrib/html-loader/commit/c82cfea0913aaf303d044c3a16f9b631dce5bc76)) 21 | 22 | ## [4.2.0](https://github.com/webpack-contrib/html-loader/compare/v4.1.0...v4.2.0) (2022-09-22) 23 | 24 | 25 | ### Features 26 | 27 | * update html minifier ([#462](https://github.com/webpack-contrib/html-loader/issues/462)) ([27a6caf](https://github.com/webpack-contrib/html-loader/commit/27a6cafeabbfd506d7e2571ea5918dd8e8cb8d29)) 28 | 29 | ## [4.1.0](https://github.com/webpack-contrib/html-loader/compare/v4.0.0...v4.1.0) (2022-07-11) 30 | 31 | 32 | ### Features 33 | 34 | * added the `scriptingEnabled` option ([#448](https://github.com/webpack-contrib/html-loader/issues/448)) ([6ed9f9c](https://github.com/webpack-contrib/html-loader/commit/6ed9f9c8df1e8ac2722bed01a9d28b660d64e744)) 35 | 36 | ### [4.0.0](https://github.com/webpack-contrib/html-loader/compare/v3.1.0...v4.0.0) (2022-06-15) 37 | 38 | ### ⚠ BREAKING CHANGES 39 | 40 | * minimum supported `Node.js` version is `14.15.0` 41 | * update `parse5` to `7.0.0` 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * handle text with ` 22 | 23 | 24 | 25 |
Foo
26 | 27 | 28 |
BAR
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Elva dressed as a fairy 56 | Elva dressed as a fairy 57 | Elva dressed as a fairy 58 | Elva dressed as a fairy 59 | Elva dressed as a fairy 60 | Elva dressed as a fairy 61 | Elva dressed as a fairy 62 | Elva dressed as a fairy 66 | Elva dressed as a fairy 67 | Elva dressed as a fairy 68 | Elva dressed as a fairy 69 | Elva dressed as a fairy 70 | Elva dressed as a fairy 79 | 80 | 81 | 82 | 83 | Flowers 84 | 85 | 86 | 89 | 90 | 91 | 92 | 93 | 94 | Smiley face 95 | 96 |
97 | First name:
98 | 99 |
100 | 101 | 102 | 103 | 107 | 108 | 112 | 113 | 116 | 117 | 121 | 122 | 123 | 124 | T ex t 129 | 130 |
131 | 132 | ]]> 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | Call me 159 | 160 | --> 161 | --> 162 | 163 | 164 | 165 | 168 | 169 |
170 |
171 | 172 | <div id = "character"> 173 | © 2007 174 | or 175 | © 2007 176 | 177 |
178 | 179 | Red dot 180 |
181 | Written by Jon Doe.
182 | Visit us at:
183 | Example.com
184 | Box 564, Disneyland
185 | USA 186 |
187 | link 188 | Start Chat 189 | Start Chat 190 | Start Chat 191 | 192 | 193 | 196 | Elva dressed as a fairy 197 | Elva dressed as a fairy 202 | 203 | Elva dressed as a fairy 204 | Test 205 | 206 | 207 | 208 | Test 209 | 210 | test 211 | test 212 | test 213 | 214 | 215 | 216 | 217 | 218 |

Text

219 |

Text

220 |

Text

221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 233 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | Elva dressed as a fairy 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | Elva dressed as a fairy 258 | test 259 | test 260 | test 261 | test 262 | test 269 | 276 | 277 | Elva dressed as a fairy 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | foo bar 298 | 299 | Text 300 | Text 301 | 302 | 303 | 304 |
305 | 306 | Visit our HTML tutorial 307 | Visit our HTML tutorial 308 | 309 | 312 | 313 | 318 | 319 |
text
320 |
text
321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 335 | 336 | multi
341 | line
342 | alt 343 | 344 | Red dot 348 | 349 | <%= name %> 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 |
447 | 450 |
451 | 452 | 455 | 456 | 457 | Smiley face 458 | Smiley face 459 | Elva dressed as a fairy 460 | Elva dressed as a fairy 461 | -------------------------------------------------------------------------------- /test/sources-option.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import { 4 | compile, 5 | execute, 6 | getCompiler, 7 | getErrors, 8 | getModuleSource, 9 | getWarnings, 10 | readAsset, 11 | } from "./helpers"; 12 | 13 | describe("'sources' option", () => { 14 | it("should work by default", async () => { 15 | const compiler = getCompiler("simple.js"); 16 | const stats = await compile(compiler); 17 | 18 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 19 | expect( 20 | execute(readAsset("main.bundle.js", compiler, stats)), 21 | ).toMatchSnapshot("result"); 22 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 23 | expect(getErrors(stats)).toMatchSnapshot("errors"); 24 | }); 25 | 26 | it('should handle "sources" tags', async () => { 27 | const compiler = getCompiler("sources.js"); 28 | const stats = await compile(compiler); 29 | 30 | expect(getModuleSource("./sources.html", stats)).toMatchSnapshot("module"); 31 | expect( 32 | execute(readAsset("main.bundle.js", compiler, stats)), 33 | ).toMatchSnapshot("result"); 34 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 35 | expect(getErrors(stats)).toMatchSnapshot("errors"); 36 | }); 37 | 38 | it("should work prefer source with tag over without", async () => { 39 | const compiler = getCompiler("simple.js", { 40 | sources: { 41 | list: [ 42 | { 43 | tag: "img", 44 | attribute: "src", 45 | type: "src", 46 | filter: () => false, 47 | }, 48 | { 49 | attribute: "src", 50 | type: "src", 51 | }, 52 | ], 53 | }, 54 | }); 55 | const stats = await compile(compiler); 56 | 57 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 58 | expect( 59 | execute(readAsset("main.bundle.js", compiler, stats)), 60 | ).toMatchSnapshot("result"); 61 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 62 | expect(getErrors(stats)).toMatchSnapshot("errors"); 63 | }); 64 | 65 | it('should work with "..." syntax', async () => { 66 | const compiler = getCompiler("simple.js", { 67 | sources: { 68 | list: [ 69 | "...", 70 | { 71 | tag: "flag-icon", 72 | attribute: "src", 73 | type: "src", 74 | }, 75 | ], 76 | }, 77 | }); 78 | const stats = await compile(compiler); 79 | 80 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 81 | expect( 82 | execute(readAsset("main.bundle.js", compiler, stats)), 83 | ).toMatchSnapshot("result"); 84 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 85 | expect(getErrors(stats)).toMatchSnapshot("errors"); 86 | }); 87 | 88 | it("should allow to add more attributes to default values", async () => { 89 | const compiler = getCompiler("simple.js", { 90 | sources: { 91 | list: [ 92 | "...", 93 | { 94 | tag: "img", 95 | attribute: "data-src", 96 | type: "src", 97 | }, 98 | { 99 | tag: "img", 100 | attribute: "data-srcset", 101 | type: "srcset", 102 | }, 103 | ], 104 | }, 105 | }); 106 | const stats = await compile(compiler); 107 | 108 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 109 | expect( 110 | execute(readAsset("main.bundle.js", compiler, stats)), 111 | ).toMatchSnapshot("result"); 112 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 113 | expect(getErrors(stats)).toMatchSnapshot("errors"); 114 | }); 115 | 116 | it('should work and override the "img" tag logic with "..."', async () => { 117 | const compiler = getCompiler("simple.js", { 118 | sources: { 119 | list: [ 120 | "...", 121 | { 122 | tag: "img", 123 | attribute: "src", 124 | type: "src", 125 | filter: () => false, 126 | }, 127 | ], 128 | }, 129 | }); 130 | const stats = await compile(compiler); 131 | 132 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 133 | expect( 134 | execute(readAsset("main.bundle.js", compiler, stats)), 135 | ).toMatchSnapshot("result"); 136 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 137 | expect(getErrors(stats)).toMatchSnapshot("errors"); 138 | }); 139 | 140 | it("should process attributes specific to a tag and attributes for any tag", async () => { 141 | const compiler = getCompiler("simple.js", { 142 | sources: { 143 | list: [ 144 | "...", 145 | { 146 | attribute: "data-src", 147 | type: "src", 148 | }, 149 | ], 150 | }, 151 | }); 152 | const stats = await compile(compiler); 153 | 154 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 155 | expect( 156 | execute(readAsset("main.bundle.js", compiler, stats)), 157 | ).toMatchSnapshot("result"); 158 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 159 | expect(getErrors(stats)).toMatchSnapshot("errors"); 160 | }); 161 | 162 | it('should handle "webpack-import" and `webpack-partial` tags', async () => { 163 | const compiler = getCompiler("webpack-import.js"); 164 | const stats = await compile(compiler); 165 | 166 | expect(getModuleSource("./webpack-import.html", stats)).toMatchSnapshot( 167 | "module", 168 | ); 169 | expect( 170 | execute(readAsset("main.bundle.js", compiler, stats)), 171 | ).toMatchSnapshot("result"); 172 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 173 | expect(getErrors(stats)).toMatchSnapshot("errors"); 174 | }); 175 | 176 | it('should not handle sources with a "boolean" notation equals "false"', async () => { 177 | const compiler = getCompiler("simple.js", { sources: false }); 178 | const stats = await compile(compiler); 179 | 180 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 181 | expect( 182 | execute(readAsset("main.bundle.js", compiler, stats)), 183 | ).toMatchSnapshot("result"); 184 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 185 | expect(getErrors(stats)).toMatchSnapshot("errors"); 186 | }); 187 | 188 | it('should handle sources with a "boolean" notation equals "true"', async () => { 189 | const compiler = getCompiler("simple.js", { sources: true }); 190 | const stats = await compile(compiler); 191 | 192 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 193 | expect( 194 | execute(readAsset("main.bundle.js", compiler, stats)), 195 | ).toMatchSnapshot("result"); 196 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 197 | expect(getErrors(stats)).toMatchSnapshot("errors"); 198 | }); 199 | 200 | it('should work with an empty "object" notations', async () => { 201 | const compiler = getCompiler("simple.js", { 202 | sources: {}, 203 | }); 204 | const stats = await compile(compiler); 205 | 206 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 207 | expect( 208 | execute(readAsset("main.bundle.js", compiler, stats)), 209 | ).toMatchSnapshot("result"); 210 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 211 | expect(getErrors(stats)).toMatchSnapshot("errors"); 212 | }); 213 | 214 | it('should work with an "object" notations', async () => { 215 | const compiler = getCompiler("simple.js", { 216 | sources: { 217 | list: [ 218 | { 219 | tag: "img", 220 | attribute: "src", 221 | type: "src", 222 | }, 223 | { 224 | tag: "img", 225 | attribute: "data-src", 226 | type: "src", 227 | }, 228 | { 229 | tag: "img", 230 | attribute: "data-srcset", 231 | type: "srcset", 232 | }, 233 | { 234 | tag: "source", 235 | attribute: "src", 236 | type: "src", 237 | }, 238 | { 239 | tag: "source", 240 | attribute: "srcset", 241 | type: "srcset", 242 | }, 243 | { 244 | tag: "flag-icon", 245 | attribute: "src", 246 | type: "src", 247 | }, 248 | { 249 | tag: "MyStrangeTag13", 250 | attribute: "src", 251 | type: "src", 252 | }, 253 | { 254 | tag: "a-", 255 | attribute: "src", 256 | type: "src", 257 | }, 258 | { 259 | tag: "a-.", 260 | attribute: "src", 261 | type: "src", 262 | }, 263 | { 264 | tag: "a--", 265 | attribute: "src", 266 | type: "src", 267 | }, 268 | { 269 | tag: "aÀ-豈", 270 | attribute: "src", 271 | type: "src", 272 | }, 273 | { 274 | tag: "aÀ-Ⰰ", 275 | attribute: "src", 276 | type: "src", 277 | }, 278 | { 279 | tag: "INVALID_TAG_NAME", 280 | attribute: "src", 281 | type: "src", 282 | }, 283 | { 284 | tag: "invalid-CUSTOM-TAG", 285 | attribute: "src", 286 | type: "src", 287 | }, 288 | ], 289 | urlFilter: (attribute, value, resourcePath) => { 290 | expect(typeof attribute).toBe("string"); 291 | expect(typeof value).toBe("string"); 292 | expect(typeof resourcePath).toBe("string"); 293 | 294 | if (value.includes("example")) { 295 | return false; 296 | } 297 | 298 | return true; 299 | }, 300 | scriptingEnabled: false, 301 | }, 302 | }); 303 | const stats = await compile(compiler); 304 | 305 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 306 | expect( 307 | execute(readAsset("main.bundle.js", compiler, stats)), 308 | ).toMatchSnapshot("result"); 309 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 310 | expect(getErrors(stats)).toMatchSnapshot("errors"); 311 | }); 312 | 313 | it("should handle all src sources in all HTML tags when tag is undefined", async () => { 314 | const compiler = getCompiler("simple.js", { 315 | sources: { 316 | list: [ 317 | { 318 | attribute: "src", 319 | type: "src", 320 | }, 321 | ], 322 | }, 323 | }); 324 | const stats = await compile(compiler); 325 | 326 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 327 | expect( 328 | execute(readAsset("main.bundle.js", compiler, stats)), 329 | ).toMatchSnapshot("result"); 330 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 331 | expect(getErrors(stats)).toMatchSnapshot("errors"); 332 | }); 333 | 334 | it("should handle all src sources in all HTML tags except img tag (testing filter option)", async () => { 335 | const compiler = getCompiler("simple.js", { 336 | sources: { 337 | list: [ 338 | { 339 | attribute: "src", 340 | type: "src", 341 | // eslint-disable-next-line no-unused-vars 342 | filter: (tag, attribute, sources) => tag.toLowerCase() !== "img", 343 | }, 344 | ], 345 | }, 346 | }); 347 | const stats = await compile(compiler); 348 | 349 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 350 | expect( 351 | execute(readAsset("main.bundle.js", compiler, stats)), 352 | ).toMatchSnapshot("result"); 353 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 354 | expect(getErrors(stats)).toMatchSnapshot("errors"); 355 | }); 356 | 357 | it("should work and supports `resolve.roots`", async () => { 358 | const compiler = getCompiler( 359 | "resolve-roots.js", 360 | {}, 361 | { 362 | resolve: { 363 | roots: [path.resolve(__dirname, "fixtures/nested/")], 364 | }, 365 | }, 366 | ); 367 | const stats = await compile(compiler); 368 | 369 | expect(getModuleSource("./resolve-roots.html", stats)).toMatchSnapshot( 370 | "module", 371 | ); 372 | expect( 373 | execute(readAsset("main.bundle.js", compiler, stats)), 374 | ).toMatchSnapshot("result"); 375 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 376 | expect(getErrors(stats)).toMatchSnapshot("errors"); 377 | }); 378 | 379 | it("should work by default with CommonJS module syntax", async () => { 380 | const compiler = getCompiler( 381 | "simple.js", 382 | {}, 383 | { 384 | module: { 385 | rules: [ 386 | { 387 | test: /\.html$/i, 388 | rules: [ 389 | { 390 | loader: path.resolve(__dirname, "../src"), 391 | options: { esModule: false }, 392 | }, 393 | ], 394 | }, 395 | { 396 | resourceQuery: /\?url$/, 397 | type: "asset/inline", 398 | }, 399 | { 400 | test: /\.(png|jpg|gif|svg|ico|eot|ttf|woff|woff2|ogg|pdf|vtt|webp|xml|webmanifest|mp3|mp4|css)$/i, 401 | resourceQuery: /^(?!.*\?url).*$/, 402 | type: "asset/resource", 403 | }, 404 | ], 405 | }, 406 | }, 407 | ); 408 | const stats = await compile(compiler); 409 | 410 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 411 | expect( 412 | execute(readAsset("main.bundle.js", compiler, stats)), 413 | ).toMatchSnapshot("result"); 414 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 415 | expect(getErrors(stats)).toMatchSnapshot("errors"); 416 | }); 417 | 418 | it("should work by default with ES module syntax", async () => { 419 | const compiler = getCompiler( 420 | "simple.js", 421 | {}, 422 | { 423 | module: { 424 | rules: [ 425 | { 426 | test: /\.html$/i, 427 | rules: [ 428 | { 429 | loader: path.resolve(__dirname, "../src"), 430 | options: { esModule: true }, 431 | }, 432 | ], 433 | }, 434 | { 435 | resourceQuery: /\?url$/, 436 | type: "asset/inline", 437 | }, 438 | { 439 | test: /\.(png|jpg|gif|svg|ico|eot|ttf|woff|woff2|ogg|pdf|vtt|webp|xml|webmanifest|mp3|mp4|css)$/i, 440 | resourceQuery: /^(?!.*\?url).*$/, 441 | type: "asset/resource", 442 | }, 443 | ], 444 | }, 445 | }, 446 | ); 447 | const stats = await compile(compiler); 448 | 449 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 450 | expect( 451 | execute(readAsset("main.bundle.js", compiler, stats)), 452 | ).toMatchSnapshot("result"); 453 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 454 | expect(getErrors(stats)).toMatchSnapshot("errors"); 455 | }); 456 | 457 | it("should work by default with ES module syntax from CommonJS module syntax from other loader", async () => { 458 | const compiler = getCompiler( 459 | "simple.js", 460 | {}, 461 | { 462 | module: { 463 | rules: [ 464 | { 465 | test: /\.html$/i, 466 | rules: [ 467 | { 468 | loader: path.resolve(__dirname, "../src"), 469 | options: { esModule: true }, 470 | }, 471 | ], 472 | }, 473 | { 474 | resourceQuery: /\?url$/, 475 | type: "asset/inline", 476 | }, 477 | { 478 | test: /\.(png|jpg|gif|svg|ico|eot|ttf|woff|woff2|ogg|pdf|vtt|webp|xml|webmanifest|mp3|mp4|css)$/i, 479 | resourceQuery: /^(?!.*\?url).*$/, 480 | type: "asset/resource", 481 | }, 482 | ], 483 | }, 484 | }, 485 | ); 486 | const stats = await compile(compiler); 487 | 488 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 489 | expect( 490 | execute(readAsset("main.bundle.js", compiler, stats)), 491 | ).toMatchSnapshot("result"); 492 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 493 | expect(getErrors(stats)).toMatchSnapshot("errors"); 494 | }); 495 | 496 | it("should work by default with CommonJS module syntax and ES module syntax from other loader", async () => { 497 | const compiler = getCompiler( 498 | "simple.js", 499 | {}, 500 | { 501 | module: { 502 | rules: [ 503 | { 504 | test: /\.html$/i, 505 | rules: [ 506 | { 507 | loader: path.resolve(__dirname, "../src"), 508 | options: { esModule: false }, 509 | }, 510 | ], 511 | }, 512 | { 513 | resourceQuery: /\?url$/, 514 | type: "asset/inline", 515 | }, 516 | { 517 | test: /\.(png|jpg|gif|svg|ico|eot|ttf|woff|woff2|ogg|pdf|vtt|webp|xml|webmanifest|mp3|mp4|css)$/i, 518 | resourceQuery: /^(?!.*\?url).*$/, 519 | type: "asset/resource", 520 | }, 521 | ], 522 | }, 523 | }, 524 | ); 525 | const stats = await compile(compiler); 526 | 527 | expect(getModuleSource("./simple.html", stats)).toMatchSnapshot("module"); 528 | expect( 529 | execute(readAsset("main.bundle.js", compiler, stats)), 530 | ).toMatchSnapshot("result"); 531 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 532 | expect(getErrors(stats)).toMatchSnapshot("errors"); 533 | }); 534 | }); 535 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | html-loader 3 | 4 | webpack 5 | 6 |
7 | 8 | [![npm][npm]][npm-url] 9 | [![node][node]][node-url] 10 | [![tests][tests]][tests-url] 11 | [![coverage][cover]][cover-url] 12 | [![discussion][discussion]][discussion-url] 13 | [![size][size]][size-url] 14 | 15 | # html-loader 16 | 17 | Exports HTML as string. HTML is minimized when the compiler demands. 18 | 19 | ## Getting Started 20 | 21 | To begin, you'll need to install `html-loader`: 22 | 23 | ```console 24 | npm install --save-dev html-loader 25 | ``` 26 | 27 | or 28 | 29 | ```console 30 | yarn add -D html-loader 31 | ``` 32 | 33 | or 34 | 35 | ```console 36 | pnpm add -D html-loader 37 | ``` 38 | 39 | Then add the loader to your `webpack` configuration. For example: 40 | 41 | **file.js** 42 | 43 | ```js 44 | import html from "./file.html"; 45 | ``` 46 | 47 | **webpack.config.js** 48 | 49 | ```js 50 | module.exports = { 51 | module: { 52 | rules: [ 53 | { 54 | test: /\.html$/i, 55 | loader: "html-loader", 56 | }, 57 | ], 58 | }, 59 | }; 60 | ``` 61 | 62 | ## Options 63 | 64 | - **[`sources`](#sources)** 65 | - **[`preprocessor`](#preprocessor)** 66 | - **[`postprocessor`](#postprocessor)** 67 | - **[`minimize`](#minimize)** 68 | - **[`esModule`](#esmodule)** 69 | 70 | ### `sources` 71 | 72 | Type: 73 | 74 | ```ts 75 | type sources = 76 | | boolean 77 | | { 78 | list?: { 79 | tag?: string; 80 | attribute?: string; 81 | type?: string; 82 | filter?: ( 83 | tag: string, 84 | attribute: string, 85 | attributes: string, 86 | resourcePath: string, 87 | ) => boolean; 88 | }[]; 89 | urlFilter?: ( 90 | attribute: string, 91 | value: string, 92 | resourcePath: string, 93 | ) => boolean; 94 | scriptingEnabled?: boolean; 95 | }; 96 | ``` 97 | 98 | Default: `true` 99 | 100 | By default every loadable attribute (for example - ``) is imported (`const img = require('./image.png')` or `new URL("./image.png", import.meta.url)`). 101 | You may need to specify loaders for images in your configuration (recommended [`asset modules`](https://webpack.js.org/guides/asset-modules/)). 102 | 103 | Supported tags and attributes: 104 | 105 | - The `src` attribute of the `audio` tag 106 | - The `src` attribute of the `embed` tag 107 | - The `src` attribute of the `img` tag 108 | - The `srcset` attribute of the `img` tag 109 | - The `src` attribute of the `input` tag 110 | - The `data` attribute of the `object` tag 111 | - The `src` attribute of the `script` tag 112 | - The `href` attribute of the `script` tag 113 | - The `xlink:href` attribute of the `script` tag 114 | - The `src` attribute of the `source` tag 115 | - The `srcset` attribute of the `source` tag 116 | - The `src` attribute of the `track` tag 117 | - The `poster` attribute of the `video` tag 118 | - The `src` attribute of the `video` tag 119 | - The `xlink:href` attribute of the `image` tag 120 | - The `href` attribute of the `image` tag 121 | - The `xlink:href` attribute of the `use` tag 122 | - The `href` attribute of the `use` tag 123 | - The `href` attribute of the `link` tag when the `rel` attribute contains `stylesheet`, `icon`, `shortcut icon`, `mask-icon`, `apple-touch-icon`, `apple-touch-icon-precomposed`, `apple-touch-startup-image`, `manifest`, `prefetch`, `preload` or when the `itemprop` attribute is `image`, `logo`, `screenshot`, `thumbnailurl`, `contenturl`, `downloadurl`, `duringmedia`, `embedurl`, `installurl`, `layoutimage` 124 | - The `imagesrcset` attribute of the `link` tag when the `rel` attribute contains `stylesheet`, `icon`, `shortcut icon`, `mask-icon`, `apple-touch-icon`, `apple-touch-icon-precomposed`, `apple-touch-startup-image`, `manifest`, `prefetch`, `preload` 125 | - The `content` attribute of the `meta` tag when the `name` attribute is `msapplication-tileimage`, `msapplication-square70x70logo`, `msapplication-square150x150logo`, `msapplication-wide310x150logo`, `msapplication-square310x310logo`, `msapplication-config`, `twitter:image` or when the `property` attribute is `og:image`, `og:image:url`, `og:image:secure_url`, `og:audio`, `og:audio:secure_url`, `og:video`, `og:video:secure_url`, `vk:image` or when the `itemprop` attribute is `image`, `logo`, `screenshot`, `thumbnailurl`, `contenturl`, `downloadurl`, `duringmedia`, `embedurl`, `installurl`, `layoutimage` 126 | - The `icon-uri` value component in `content` attribute of the `meta` tag when the `name` attribute is `msapplication-task` 127 | 128 | #### `boolean` 129 | 130 | - true: Enables processing of all default tags and attributes 131 | - false: Disables processing entirely 132 | 133 | **webpack.config.js** 134 | 135 | ```js 136 | module.exports = { 137 | module: { 138 | rules: [ 139 | { 140 | test: /\.html$/i, 141 | loader: "html-loader", 142 | options: { 143 | // Disables attributes processing 144 | sources: false, 145 | }, 146 | }, 147 | ], 148 | }, 149 | }; 150 | ``` 151 | 152 | #### `object` 153 | 154 | Allows you to specify which tags and attributes to process, filter them, filter URLs and process sources starting with `/`. 155 | 156 | For example: 157 | 158 | **webpack.config.js** 159 | 160 | ```js 161 | module.exports = { 162 | module: { 163 | rules: [ 164 | { 165 | test: /\.html$/i, 166 | loader: "html-loader", 167 | options: { 168 | sources: { 169 | list: [ 170 | // All default supported tags and attributes 171 | "...", 172 | { 173 | tag: "img", 174 | attribute: "data-src", 175 | type: "src", 176 | }, 177 | { 178 | tag: "img", 179 | attribute: "data-srcset", 180 | type: "srcset", 181 | }, 182 | ], 183 | urlFilter: (attribute, value, resourcePath) => { 184 | // The `attribute` argument contains a name of the HTML attribute. 185 | // The `value` argument contains a value of the HTML attribute. 186 | // The `resourcePath` argument contains a path to the loaded HTML file. 187 | 188 | if (/example\.pdf$/.test(value)) { 189 | return false; 190 | } 191 | 192 | return true; 193 | }, 194 | }, 195 | }, 196 | }, 197 | ], 198 | }, 199 | }; 200 | ``` 201 | 202 | #### `list` 203 | 204 | Type: 205 | 206 | ```ts 207 | type list = { 208 | tag?: string; 209 | attribute?: string; 210 | type?: string; 211 | filter?: ( 212 | tag: string, 213 | attribute: string, 214 | attributes: string, 215 | resourcePath: string, 216 | ) => boolean; 217 | }[]; 218 | ``` 219 | 220 | Default: [supported tags and attributes](#sources). 221 | 222 | Allows to setup which tags and attributes to process and how, as well as the ability to filter some of them. 223 | 224 | Using `...` syntax allows you to extend [default supported tags and attributes](#sources). 225 | 226 | For example: 227 | 228 | **webpack.config.js** 229 | 230 | ```js 231 | module.exports = { 232 | module: { 233 | rules: [ 234 | { 235 | test: /\.html$/i, 236 | loader: "html-loader", 237 | options: { 238 | sources: { 239 | list: [ 240 | // All default supported tags and attributes 241 | "...", 242 | { 243 | tag: "img", 244 | attribute: "data-src", 245 | type: "src", 246 | }, 247 | { 248 | tag: "img", 249 | attribute: "data-srcset", 250 | type: "srcset", 251 | }, 252 | { 253 | // Tag name 254 | tag: "link", 255 | // Attribute name 256 | attribute: "href", 257 | // Type of processing, can be `src` or `scrset` 258 | type: "src", 259 | // Allow to filter some attributes 260 | filter: (tag, attribute, attributes, resourcePath) => { 261 | // The `tag` argument contains a name of the HTML tag. 262 | // The `attribute` argument contains a name of the HTML attribute. 263 | // The `attributes` argument contains all attributes of the tag. 264 | // The `resourcePath` argument contains a path to the loaded HTML file. 265 | 266 | if (/my-html\.html$/.test(resourcePath)) { 267 | return false; 268 | } 269 | 270 | if (!/stylesheet/i.test(attributes.rel)) { 271 | return false; 272 | } 273 | 274 | if ( 275 | attributes.type && 276 | attributes.type.trim().toLowerCase() !== "text/css" 277 | ) { 278 | return false; 279 | } 280 | 281 | return true; 282 | }, 283 | }, 284 | ], 285 | }, 286 | }, 287 | }, 288 | ], 289 | }, 290 | }; 291 | ``` 292 | 293 | If the tag name is not specified it will process all the tags. 294 | 295 | > You can use your custom filter to specify HTML elements to be processed. 296 | 297 | For example: 298 | 299 | **webpack.config.js** 300 | 301 | ```js 302 | module.exports = { 303 | module: { 304 | rules: [ 305 | { 306 | test: /\.html$/i, 307 | loader: "html-loader", 308 | options: { 309 | sources: { 310 | list: [ 311 | { 312 | // Attribute name 313 | attribute: "src", 314 | // Type of processing, can be `src` or `scrset` 315 | type: "src", 316 | // Allow to filter some attributes (optional) 317 | filter: (tag, attribute, attributes, resourcePath) => 318 | // The `tag` argument contains a name of the HTML tag. 319 | // The `attribute` argument contains a name of the HTML attribute. 320 | // The `attributes` argument contains all attributes of the tag. 321 | // The `resourcePath` argument contains a path to the loaded HTML file. 322 | 323 | // choose all HTML tags except img tag 324 | tag.toLowerCase() !== "img", 325 | }, 326 | ], 327 | }, 328 | }, 329 | }, 330 | ], 331 | }, 332 | }; 333 | ``` 334 | 335 | Filter can also be used to extend the supported elements and attributes. 336 | 337 | For example, filter can help process meta tags that reference assets: 338 | 339 | ```js 340 | module.exports = { 341 | module: { 342 | rules: [ 343 | { 344 | test: /\.html$/i, 345 | loader: "html-loader", 346 | options: { 347 | sources: { 348 | list: [ 349 | { 350 | tag: "meta", 351 | attribute: "content", 352 | type: "src", 353 | filter: (tag, attribute, attributes, resourcePath) => { 354 | if ( 355 | attributes.value === "og:image" || 356 | attributes.name === "twitter:image" 357 | ) { 358 | return true; 359 | } 360 | 361 | return false; 362 | }, 363 | }, 364 | ], 365 | }, 366 | }, 367 | }, 368 | ], 369 | }, 370 | }; 371 | ``` 372 | 373 | > [!NOTE] 374 | > 375 | > source with a `tag` option takes precedence over source without. 376 | 377 | Filter can be used to disable default sources. 378 | 379 | For example: 380 | 381 | ```js 382 | module.exports = { 383 | module: { 384 | rules: [ 385 | { 386 | test: /\.html$/i, 387 | loader: "html-loader", 388 | options: { 389 | sources: { 390 | list: [ 391 | "...", 392 | { 393 | tag: "img", 394 | attribute: "src", 395 | type: "src", 396 | filter: () => false, 397 | }, 398 | ], 399 | }, 400 | }, 401 | }, 402 | ], 403 | }, 404 | }; 405 | ``` 406 | 407 | #### `urlFilter` 408 | 409 | Type: 410 | 411 | ```ts 412 | type urlFilter = ( 413 | attribute: string, 414 | value: string, 415 | resourcePath: string, 416 | ) => boolean; 417 | ``` 418 | 419 | Default: `undefined` 420 | 421 | Allow to filter URLs. All filtered URLs will not be resolved (left in the code as they were written). 422 | Non-requestable sources (for example ``) are not handled by default. 423 | 424 | ```js 425 | module.exports = { 426 | module: { 427 | rules: [ 428 | { 429 | test: /\.html$/i, 430 | loader: "html-loader", 431 | options: { 432 | sources: { 433 | urlFilter: (attribute, value, resourcePath) => { 434 | // The `attribute` argument contains a name of the HTML attribute. 435 | // The `value` argument contains a value of the HTML attribute. 436 | // The `resourcePath` argument contains a path to the loaded HTML file. 437 | 438 | if (/example\.pdf$/.test(value)) { 439 | return false; 440 | } 441 | 442 | return true; 443 | }, 444 | }, 445 | }, 446 | }, 447 | ], 448 | }, 449 | }; 450 | ``` 451 | 452 | #### `scriptingEnabled` 453 | 454 | Type: 455 | 456 | ```ts 457 | type scriptingEnabled = boolean; 458 | ``` 459 | 460 | Default: `true` 461 | 462 | By default, the parser in `html-loader` interprets content inside `