feat: HMR for HTML modules with DOM patching for extracted pages#21011
Conversation
🦋 Changeset detectedLatest commit: b205a24 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is packaged and the instant preview is available (04be1fe). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@04be1fe
yarn add -D webpack@https://pkg.pr.new/webpack@04be1fe
pnpm add -D webpack@https://pkg.pr.new/webpack@04be1fe |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #21011 +/- ##
==========================================
+ Coverage 91.65% 91.67% +0.02%
==========================================
Files 573 574 +1
Lines 59912 60030 +118
Branches 16169 16211 +42
==========================================
+ Hits 54914 55035 +121
+ Misses 4998 4995 -3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds Hot Module Replacement behavior for experimental HTML modules by emitting an HMR-aware JS shim (self-accept + optional DOM patching when the HTML is extracted), and expands the hot test suite to cover common HTML dependency patterns.
Changes:
- Emit an HMR self-accepting JS shim for
.htmlmodules, with DOM patching (document.body.innerHTML/document.title) when the HTML is extracted. - Prevent HTML module concatenation when HMR is enabled to preserve per-module
module.hotscope. - Add hot test cases for import-only HTML, extracted HTML DOM patching + reload fallback, and external/inline
<script>/<style>/<link rel="stylesheet">scenarios; extend the test DOM harness accordingly.
Reviewed changes
Copilot reviewed 63 out of 63 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/html/HtmlGenerator.js | Emits HMR-aware HTML JS shim + concatenation bailout + hash invalidation when module.hot toggles. |
| test/helpers/FakeDocument.js | Extends fake DOM to support document.title and element.innerHTML for DOM-patch test assertions. |
| .changeset/html-module-hmr.md | Changeset entry documenting HTML-module HMR behavior. |
| test/hotCases/html/html-import/webpack.config.js | Hot case config for importing an HTML module as a string. |
| test/hotCases/html/html-import/page.html | Versioned HTML input for hot updates (v1→v3). |
| test/hotCases/html/html-import/index.js | Verifies re-requiring an HTML module returns updated content across multiple HMR cycles. |
| test/hotCases/html/html-extract/webpack.config.js | Hot case config for extracted HTML module DOM patching path. |
| test/hotCases/html/html-extract/test.filter.js | Filters extract DOM-patching test to target: "web". |
| test/hotCases/html/html-extract/test.config.js | JSDOM environment + reload counter plumbing for extract DOM-patch assertions. |
| test/hotCases/html/html-extract/page.html | Versioned extracted HTML input for DOM patching (v1→v3). |
| test/hotCases/html/html-extract/index.js | Verifies document.body/document.title patching without reload for extracted HTML updates. |
| test/hotCases/html/html-head-change/webpack.config.js | Hot case config for extracted HTML with <head> mutation beyond <title>. |
| test/hotCases/html/html-head-change/test.filter.js | Filters head-change reload fallback test to target: "web". |
| test/hotCases/html/html-head-change/test.config.js | JSDOM environment + reload counter plumbing for reload-fallback assertions. |
| test/hotCases/html/html-head-change/page.html | Versioned HTML input with <meta> head change (v1→v2). |
| test/hotCases/html/html-head-change/index.js | Verifies head changes beyond <title> trigger reload fallback instead of DOM patch. |
| test/hotCases/html/html-comments/webpack.config.js | Hot case config for extracted HTML containing tag-like markers in comments. |
| test/hotCases/html/html-comments/test.filter.js | Filters comment-decoy test to target: "web". |
| test/hotCases/html/html-comments/test.config.js | JSDOM environment + reload counter plumbing for comment-decoy assertions. |
| test/hotCases/html/html-comments/page.html | Versioned HTML with decoy <title>/<body> markers inside comments (v1→v2). |
| test/hotCases/html/html-comments/index.js | Verifies DOM patching ignores <body>/<title> markers inside HTML comments. |
| test/hotCases/html/html-with-inline-style/webpack.config.js | Hot case config for HTML with inline <style> routed through CSS pipeline. |
| test/hotCases/html/html-with-inline-style/test.filter.js | Filters inline-style test to target: "web" for CSS URL rewriting assertions. |
| test/hotCases/html/html-with-inline-style/test.config.js | JSDOM environment for inline-style hot update test. |
| test/hotCases/html/html-with-inline-style/page.html | Versioned HTML where inline <style> body changes (v1→v2). |
| test/hotCases/html/html-with-inline-style/index.js | Verifies inline style changes are reflected in the rewritten exported HTML string after HMR. |
| test/hotCases/html/html-with-inline-script/webpack.config.js | Hot case config for HTML with inline <script> turned into chunk-referenced code. |
| test/hotCases/html/html-with-inline-script/test.filter.js | Filters inline-script test to target: "web" for chunk-loading runtime. |
| test/hotCases/html/html-with-inline-script/test.config.js | JSDOM environment for inline-script hot update test. |
| test/hotCases/html/html-with-inline-script/page.html | Versioned HTML where inline <script> body changes (v1→v2). |
| test/hotCases/html/html-with-inline-script/index.js | Verifies inline script changes update the rewritten HTML (title + chunk src shape) after HMR. |
| test/hotCases/html/html-with-link-stylesheet/webpack.config.js | Hot case config for <link rel="stylesheet"> becoming a CSS entry chunk. |
| test/hotCases/html/html-with-link-stylesheet/test.filter.js | Filters link-stylesheet test to target: "web" for CSS extraction behavior. |
| test/hotCases/html/html-with-link-stylesheet/test.config.js | JSDOM environment for link-stylesheet hot update test. |
| test/hotCases/html/html-with-link-stylesheet/style.css | Versioned CSS input for stylesheet hot updates (red→blue). |
| test/hotCases/html/html-with-link-stylesheet/page.html | Versioned HTML referencing external stylesheet (v1→v2). |
| test/hotCases/html/html-with-link-stylesheet/index.js | Verifies rewritten stylesheet href points to bundled CSS chunk and HTML updates on HMR. |
| test/hotCases/html/html-with-script-src/webpack.config.js | Hot case config for <script src> becoming its own entry chunk. |
| test/hotCases/html/html-with-script-src/test.filter.js | Filters script-src test to target: "web" for chunk-loading runtime. |
| test/hotCases/html/html-with-script-src/test.config.js | JSDOM environment for script-src hot update test. |
| test/hotCases/html/html-with-script-src/page.html | Versioned HTML referencing external script (v1→v2). |
| test/hotCases/html/html-with-script-src/index.js | Verifies rewritten <script src> points to bundled chunk URL and HTML updates on HMR. |
| test/hotCases/html/html-with-script-src/external.js | Versioned external script entry that self-accepts on HMR (v1→v2). |
| test/hotCases/html/html-inline-script-and-style/webpack.config.js | Hot case config combining inline <style> (CSS pipeline) + inline <script> (chunk). |
| test/hotCases/html/html-inline-script-and-style/test.filter.js | Filters combined inline script/style test to target: "web". |
| test/hotCases/html/html-inline-script-and-style/test.config.js | JSDOM environment for combined inline script/style hot update test. |
| test/hotCases/html/html-inline-script-and-style/page.html | Versioned HTML with both inline <style> and inline <script> changes (v1→v2). |
| test/hotCases/html/html-inline-script-and-style/index.js | Verifies both inline style and inline script handling + HTML string update on HMR. |
| test/hotCases/html/html-inline-update-tracking/webpack.config.js | Hot case config for inspecting updated module IDs when inline data-URI modules change. |
| test/hotCases/html/html-inline-update-tracking/test.filter.js | Filters inline update-tracking test to target: "web". |
| test/hotCases/html/html-inline-update-tracking/test.config.js | JSDOM environment for inline update-tracking test. |
| test/hotCases/html/html-inline-update-tracking/page.html | Versioned HTML where inline <style> and <script> bodies both change (v1→v2). |
| test/hotCases/html/html-inline-update-tracking/index.js | Verifies module.hot.check(true) reports HTML updated + old inline data-URI modules removed. |
| test/hotCases/html/html-external-update-tracking/webpack.config.js | Hot case config for external <script src> + external stylesheet update behavior. |
| test/hotCases/html/html-external-update-tracking/test.filter.js | Filters external update-tracking test to target: "web". |
| test/hotCases/html/html-external-update-tracking/test.config.js | JSDOM environment for external update-tracking test. |
| test/hotCases/html/html-external-update-tracking/style.css | Versioned external stylesheet content for update-tracking (red→blue). |
| test/hotCases/html/html-external-update-tracking/page.html | HTML referencing external script + stylesheet for update-tracking scenario. |
| test/hotCases/html/html-external-update-tracking/index.js | Verifies HTML module runtime sees an empty changeset when only external resources update. |
| test/hotCases/html/html-external-update-tracking/external.js | Versioned external script entry that self-accepts on HMR (1→2). |
| test/hotCases/html/html-with-output-module/webpack.config.js | Hot case config for HTML modules under experiments.outputModule. |
| test/hotCases/html/html-with-output-module/page.html | Versioned HTML input for ESM output test (v1→v2). |
| test/hotCases/html/html-with-output-module/index.js | Verifies HTML module can be re-imported after HMR in ESM output (accept done by importer). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "var __webpack_stripped__ = __webpack_strip_comments__(__webpack_html__);", | ||
| "var __webpack_body_m__ = /<body[^>]*>([\\s\\S]*?)<\\/body>/i.exec(__webpack_stripped__);", | ||
| "if (__webpack_body_m__ && document.body) document.body.innerHTML = __webpack_body_m__[1];", | ||
| "var __webpack_title_m__ = /<title[^>]*>([\\s\\S]*?)<\\/title>/i.exec(__webpack_stripped__);", | ||
| "if (__webpack_title_m__) document.title = __webpack_title_m__[1];" |
| "}", | ||
| // Capture this evaluation's head (sans title, sans comments) | ||
| // on dispose so the next module instance can diff against it. | ||
| "module.hot.dispose(function (data) { data.__webpack_html_updated__ = true; data.__webpack_head__ = __webpack_extract_head__(__webpack_html__); });" |
| // FakeDocument doesn't parse HTML — the HMR DOM-patch test only | ||
| // asserts on the raw string passed in, so storing it verbatim is | ||
| // enough for verification. | ||
| this._innerHTML = String(value || ""); |
| "webpack": minor | ||
| --- | ||
|
|
||
| Add HMR support for HTML modules. The JS shim a `.html` module exports now self-accepts when `HotModuleReplacementPlugin` is enabled, so re-requiring the module after an HMR cycle returns the updated content. When the module is also being extracted to a real `.html` file (`module.generator.html.extract: true`, or the implicit default for HTML entry points), the shim additionally patches `document.body.innerHTML` and `document.title` on every hot update, so the rendered page reflects the new HTML without a full reload. If the `<head>` changes beyond `<title>` (e.g. a new `<meta>` tag, a swapped `<link rel=icon>`, an inline `<style>` block), the shim falls back to `window.location.reload()` — webpack injects its own runtime scripts and stylesheet links into the head on initial load, so a blanket head replacement isn't safe; in a dev-server context the reloaded page picks up the new head from the regular `page.html` chunk re-emitted on the latest rebuild. |
There was a problem hiding this comment.
The changeset has since been rewritten to a single imperative sentence (Add HMR support for HTML modules with body/title DOM patching on update.) to comply with the one-sentence ≤80-char changeset rule, so the flagged "The JS shim a .html module exports" wording no longer exists. The detailed rationale now lives in the PR body instead.
Generated by Claude Code
Merging this PR will improve performance by 32.91%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
01b6507 to
1d67496
Compare
| @@ -320,7 +329,12 @@ class HtmlGenerator extends Generator { | |||
|
|
|||
There was a problem hiding this comment.
Good catch — fixed in a214a07. The extract HMR shim now keeps the [webpack/auto] placeholder in the body HTML and resolves it at runtime against __webpack_require__.p (the shim builds __webpack_html__ via .split("__WEBPACK_CSS_PUBLIC_PATH_AUTO__").join(__webpack_require__.p), and generate adds RuntimeGlobals.publicPath to the runtime requirements). So a DOM-patched document.body now points at the output root regardless of where the extracted .html document lives, matching the undo-path semantics HtmlModulesPlugin applies to the extracted asset. Added test/hotCases/html/html-extract-public-path/ covering publicPath:"auto" + a body <script src> to lock in that the placeholder is resolved at runtime and never leaks into the patched DOM.
Generated by Claude Code
There was a problem hiding this comment.
Update: after rebasing this branch onto the latest main, the runtime-resolution fix I described above was dropped. main reworked HTML placeholder handling — chunk URLs are emitted as sentinels and a JavascriptModulesPlugin.render tap resolves them and blanket-replaces [webpack/auto] to root-relative URLs across the entire rendered JS chunk at build time. That tap is incompatible with a runtime .split("[webpack/auto]").join(__webpack_require__.p) in the shim (it rewrites the literal placeholder argument itself), and it makes the JS export deliberately root-relative. The HMR DOM-patch now reuses that same build-time-resolved HTML, so it's consistent with the non-HMR JS export rather than fighting main's mechanism. Added test/hotCases/html/html-extract-body-asset/ to cover a body <script src> being DOM-patched as a resolved chunk URL.
Generated by Claude Code
a214a07 to
64b87ae
Compare
| // JS export: resolve `[webpack/auto]` to root-relative URLs. | ||
| const generated = this._renderHtml(module, generateContext, ""); | ||
|
|
||
| /** @type {string} */ | ||
| let sourceContent; |
There was a problem hiding this comment.
This is the same concern as the (now-outdated) thread above, re-surfaced after the rebase — and the suggested fix unfortunately can't work on current main. main's JavascriptModulesPlugin.render tap (added in #21018) resolves chunk-URL sentinels and then does .split(CssUrlDependency.PUBLIC_PATH_AUTO).join("") across the entire rendered JS chunk at build time. So:
- Keeping the placeholder and emitting
…split("__WEBPACK_CSS_PUBLIC_PATH_AUTO__").join(__webpack_require__.p)in the shim doesn't survive — the tap rewrites that literalsplit(...)argument tosplit(""), which shatters the HTML at runtime (verified: it splits the string into single characters). I tried exactly this and it broke. - The tap makes the JS export of an HTML module deliberately root-relative regardless of document location. The DOM-patch now reuses that same JS-export HTML, so it's consistent with
import html from "./page.html"rather than diverging from it.
The residual subdirectory case (output.htmlFilename: "pages/[name].html" + publicPath: "auto") is therefore a property of main's JS-export URL resolution — the extracted .html asset uses an undo-path while the JS export is root-relative — not something this HMR PR introduces. Making the DOM-patch use undo-path semantics would mean changing that build-time mechanism in HtmlModulesPlugin/the render tap, which is out of scope here. Happy to open a follow-up for it if you'd like the JS export (and thus the DOM-patch) to honor the document's undo path.
Generated by Claude Code
| "var __webpack_extract_head__ = function (h) {", | ||
| Template.indent([ | ||
| // Diff the head with comments masked out so a comment-only | ||
| // head edit doesn't needlessly force a full reload. | ||
| 'var head = __webpack_extract__(__webpack_mask_comments__(h), "head");', | ||
| 'return head === null ? "" : head.replace(/<title[^>]*>[\\s\\S]*?<\\/title>/i, "").trim();' |
There was a problem hiding this comment.
Fixed in 2eec875. __webpack_extract_head__ now extracts from the original HTML (__webpack_extract__ already masks internally for boundary detection) and masks only the small extracted head substring for the comment-insensitive diff, so the whole document is no longer masked twice per dispose/eval. Behavior is unchanged — html-head-change and html-comments (the comment-only-head-edit and decoy cases) still pass.
Generated by Claude Code
| 'var head = __webpack_extract__(__webpack_mask_comments__(h), "head");', | ||
| 'return head === null ? "" : head.replace(/<title[^>]*>[\\s\\S]*?<\\/title>/i, "").trim();' |
There was a problem hiding this comment.
This re-surfaced because the previous commit (5985477) was a temporary revert of the head-mask optimization while I was bisecting a flaky CI failure. The current HEAD (c3c5935) re-applies the fix you originally suggested: __webpack_extract_head__ now calls __webpack_extract__(h, "head") and masks only the returned head substring before stripping <title>/trimming — no full-document double-mask. So this is already addressed on the latest commit. (The flaky a-partition CI failure turned out to be unrelated to this PR — a tree byte-identical to a previously-green commit also failed it.)
Generated by Claude Code
c3c5935 to
0bc17fc
Compare
| 'var __webpack_mask_comments__ = function (h) { return h.replace(/<!--[\\s\\S]*?-->/g, function (c) { return c.replace(/[^\\n]/g, " "); }); };', | ||
| // Slice the inner content of `<tag>…</tag>` out of `h`, | ||
| // using the masked copy only to locate the tag boundaries. | ||
| "var __webpack_extract__ = function (h, tag) {", | ||
| Template.indent([ | ||
| "var masked = __webpack_mask_comments__(h);", | ||
| 'var open = new RegExp("<" + tag + "[^>]*>", "i").exec(masked);', | ||
| "if (!open) return null;", | ||
| "var start = open.index + open[0].length;", | ||
| 'var close = new RegExp("</" + tag + ">", "i").exec(masked.slice(start));', | ||
| "if (!close) return null;", |
There was a problem hiding this comment.
Fixed in 6fafb26. The shim now precomputes a single masked copy per evaluation — var __webpack_masked_html__ = __webpack_mask_comments__(__webpack_html__); — and __webpack_extract__(tag) / __webpack_extract_head__() close over it for all head/body/title boundary searches (and the dispose-time head diff), so the full document is masked once per update instead of ~4×. Only the small extracted head is masked again (for the comment-insensitive reload diff). Behavior unchanged — html-extract, html-head-change, html-comments, and html-extract-body-asset all pass.
Generated by Claude Code
The JS shim emitted for `.html` modules now self-accepts when HotModuleReplacementPlugin is enabled, so re-requiring the module after an HMR cycle returns the updated content. For modules that are also extracted to a real `.html` file, the shim additionally patches `document.body.innerHTML` and `document.title` on every hot update so the rendered page reflects the new HTML without a full reload. The HtmlGenerator bails out of module concatenation when HMR is active, since the per-module `module.hot` scope is required for self-acceptance. Adds hot test cases covering the import (no-extract), extract+DOM-patch, inline-style and inline-script scenarios. FakeDocument gains minimal `innerHTML` and `title` support so the DOM-patch path is verifiable in the test harness.
…module Adds three more hot test cases: - html-with-script-src — HTML referencing an external `<script src>`, verifying that the parser-generated script entry chunk and the HTML module's self-acceptance survive an HMR cycle. - html-with-link-stylesheet — HTML referencing a `<link rel=stylesheet>`, verifying that the CSS entry chunk created from the link tag still resolves correctly after an HMR cycle. - html-with-output-module — same flow exercised under ESM output (`output.module: true`, `chunkFormat: "module"`, `.mjs` filenames) so the JS shim's `module.hot.accept()` is verified in the ESM wrapper as well as the CJS one.
Adds three more hot test cases that document and verify *which* modules
land in `module.hot.check`'s update list when different parts of an HTML
module change:
- html-inline-script-and-style — HTML with both inline `<style>` and
inline `<script>` in the same document; verifies the HMR cycle
succeeds with both forms present and that the rewritten output shape
(inlined CSS body + extracted `<script src=…>` URL) is preserved
across updates.
- html-inline-update-tracking — only changes the inline `<style>` and
`<script>` bodies between updates and asserts the update list:
1. the HTML module itself (its rewritten string changed), and
2. the OLD `data:text/css,…` / `data:text/javascript;base64,…`
modules (removed because the body change reshapes the data-URI
identifier).
The NEW data-URI modules aren't in the changeset since they hadn't
been loaded at the previous evaluation — they're added on demand.
- html-external-update-tracking — changes only `external.js` and
`style.css` (the targets of `<script src>` / `<link
rel=stylesheet>`), with named chunk filenames so the URLs stay
stable. The HTML module's runtime sees an empty update list — proving
the HTML module is NOT re-emitted just because external resources
changed. The actual updates flow through the separate entry chunks'
own runtimes.
The DOM-patch path in the HTML module HMR shim only safely covers `document.body.innerHTML` and `document.title`. Patching the entire `<head>` would also tear down the runtime `<script>` tags and stylesheet `<link>` tags webpack injects there on initial load. When the new HTML's head (sans title) differs from the captured head of the previous evaluation, the shim now calls `window.location.reload()` instead of attempting a DOM patch. In a dev-server context this picks up the new head from the regular `page.html` chunk re-emitted on the latest rebuild — hot-update chunks intentionally skip emitting `.html`, but the regular non-hot-update chunk is still produced on every rebuild. Body / title-only changes still take the DOM-patch path; the existing `html-extract` test asserts `window.location.reload` is never invoked across its three updates. Adds an `html-head-change` hot case that flips a `<meta>` attribute and verifies the shim falls back to a reload.
The DOM-patch HMR shim used three regexes at runtime to extract the `<body>` content, `<title>` text, and the `<head>` content sans title. Regex-based HTML parsing is fragile by nature (nested `</body>` inside attributes or comments, mixed-case tags, weird whitespace, …) and the runtime parsing cost was paid on every hot update. Walk the *rewritten* HTML once at build time with the existing `walkHtmlTokens` tokenizer to locate the three byte ranges, slice them out, and emit them as plain string literals in the shim. The runtime side is now just: var __webpack_body__ = "..."; var __webpack_title__ = "..."; var __webpack_head__ = "..."; …with the patch / reload decision still made on `module.hot.data`. Zero regex, zero parsing, no tokenizer ship to the client; the generated shim is also significantly smaller (no `__webpack_get_head__` helper, no inline regex literals). The first `<title>` is only captured while inside `<head>`, so a stray SVG `<title>` in the body isn't mistakenly used as the document title. The first `<head>` / `<body>` are taken; nested or out-of-document duplicates are ignored.
…irst Bring back the runtime regex-based extraction for `<body>`, `<title>` and `<head>` content (smaller build-time bookkeeping, no second walker pass at generate time) — but strip HTML comments first so decoy tags inside `<!-- … -->` can't fool the non-greedy `*?` in the tag regexes. Without the comment strip, a comment like <!-- <body>example</body> --> would be picked up as the actual `<body>` because the `*?` stops at the FIRST `</body>` it sees. With h.replace(/<!--[\s\S]*?-->/g, "") run before any tag-extraction regex, the decoys disappear and the real `<body>` / `<title>` / `<head>` ranges win. Adds a hot test (`html-comments`) whose source HTML has decoy `<title>` and `<body>` markers inside HTML comments — the DOM-patch path must land the REAL body / title content, not the decoys, and the head-vs-decoy diff must stay stable (so the reload branch isn't spuriously triggered).
The HMR shim was hand-indenting each line with hard-coded `\t` / `\t\t` / `\t\t\t` prefixes. Webpack already exposes `Template.indent` and `Template.asString` for exactly this — `CssModulesPlugin`'s HMR snippet uses the same pattern. Switching over removes the manual indentation arithmetic, makes the nesting explicit in source (the indent function calls match the indent structure of the generated code), and keeps the emitted output byte-identical.
AGENTS.md (after #21005) caps changeset descriptions at one sentence ≤ 80 characters in the imperative — multi-paragraph rationale belongs in the PR body, not the changeset.
Address Copilot review on the HTML-module HMR shim: - The shim stripped HTML comments before extracting the body/title, so the DOM-patched `document.body.innerHTML` dropped real comments that live inside `<body>` — diverging from what a full page reload renders. Switch from deleting comments to *masking* them (replace their characters with equal-length spaces). Masking keeps string offsets stable, so tag boundaries are still located on a copy where `<body>`/`</body>`/`<title>` inside a comment can't fool the regex, while the body/title content is sliced back out of the original HTML with comments intact. - Drop the write-only `data.__webpack_html_updated__` dispose flag — nothing read it. - FakeDocument: coerce `innerHTML` with a nullish check so `0`/`false` round-trip like the real DOM instead of collapsing to "".
The extracted HMR shim patches document.body with the JS-export HTML, whose chunk URLs are resolved at build time (sentinel + render tap). Add a hot case asserting a body <script src> is patched as a resolved chunk URL with no unresolved sentinel and without a full reload.
__webpack_extract_head__ pre-masked the whole HTML and then __webpack_extract__ masked it again. Extract from the original HTML and mask only the small extracted head substring instead.
This reverts commit 2eec875.
This reverts commit 5985477.
main's new module.parser.html.sources (#21022) encodes inline <style> as data:text/css;base64,… instead of data:text/css,…. Match the new prefix; the old style module is uniquely identified by prefix in the changeset, so no base64 decoding is needed.
The extract shim masked the full HTML on every tag-boundary search (head/body/title + dispose). Precompute one masked copy per evaluation and reuse it, so a large page isn't re-scanned per section.
6fafb26 to
d1b7f27
Compare
Use runtimeTemplate.renderConst()/basicFunction()/returningFunction() so the HMR shim emits const and arrow functions when the target environment supports them, falling back to var/function otherwise.
Collapse the multi-line explanatory comments in CssParser to one-line sentences; no behavior change.
| // JS export: resolve `[webpack/auto]` to root-relative URLs. | ||
| const generated = this._renderHtml(module, generateContext, ""); | ||
|
|
Summary
Experimental HTML modules had no Hot Module Replacement support: editing a
.htmlentry (or its inline/external dependencies) forced a full rebuild with no in-place update.This adds HMR for HTML modules:
.htmlmodules now self-accepts whenHotModuleReplacementPluginis enabled, so re-requiring the module after an HMR cycle returns the updated content..htmlfile, the shim patchesdocument.body.innerHTMLanddocument.titleon every hot update so the rendered page reflects the new HTML without a full reload. Changes to<head>beyond<title>(a new<meta>, a swapped<link rel=icon>, …) can't be safely DOM-patched, so the shim falls back to a full reload.<body>/<title>inside a comment can't fool the extractor while real comments inside<body>are preserved verbatim in the patched DOM — matching what a full reload renders.HtmlGeneratorbails out of module concatenation when HMR is active, since the per-modulemodule.hotscope is required for self-acceptance.Asset/chunk URLs in the patched body inherit the existing build-time placeholder resolution (the chunk-URL sentinel +
[webpack/auto]render tap added tomain), so the DOM-patched body uses the same resolved URLs as the module's JS export.FakeDocumentgains minimalinnerHTMLandtitlesupport so the DOM-patch path is verifiable in the test harness. The branch is rebased on the latestmain.What kind of change does this PR introduce?
feat
Did you add tests for your changes?
Yes — hot test cases under
test/hotCases/html/covering import (no-extract), extract + DOM-patch, head-change reload fallback, comment decoys / preservation, a body<script src>chunk-URL DOM-patch (html-extract-body-asset), inline<style>, inline<script>, combined inline script+style, external<script src>,<link rel="stylesheet">, inline/external update-tracking, andoutput.module.Does this PR introduce a breaking change?
No. HTML modules are experimental and the new behavior only activates when HMR is enabled.
If relevant, what needs to be documented once your changes are merged or what have you already documented?
The experimental HTML-module docs should note that HMR is now supported, that extracted pages are DOM-patched (with a full-reload fallback for
<head>changes beyond<title>), and that module concatenation is disabled for HTML modules while HMR is active.Use of AI
Claude Code was used to draft the implementation, the test cases, and this PR description, and to address review feedback, all under human review.