feat: native HTML output features (favicon attributes, manifest, CSP, tag inject/transform hooks) - #21487
Conversation
Each `<link rel>` in `output.html.favicon` can now be an object with the
icon `href` plus extra link attributes (`sizes`, `media`, `color`, `type`,
`crossorigin`), or an array of icons for several variants under one rel
(e.g. multiple `sizes`, or light/dark `media`). The bare-string path form
still works and is normalized to `{ href }`.
`output.html.manifest` injects a `<link rel="manifest">` into webpack-generated pages. A string links an existing `.webmanifest` file; an object is serialized to a base64 `data:application/manifest+json` URL routed to the `asset/webmanifest` type by a new default rule, so its `icons`/`screenshots` `src` paths are emitted as hashed assets and the manifest itself is emitted content-hashed. A function returns either form per page. Authored pages are left untouched.
`output.html.csp` injects a `<meta http-equiv="Content-Security-Policy">` into every webpack-emitted HTML page once inline content is final. `true` uses a strict baseline (`script-src 'self'`, `style-src 'self'`, `object-src 'none'`, `base-uri 'self'`) and appends a sha256 hash of each inline `<script>`/`<style>` to `script-src`/`style-src`. The object form adds `policy` (override/extend directives), `hashFunction` (sha256/384/512) and `nonce` (a placeholder added to injected tags and as a `'nonce-…'` source for per-request server substitution). A page that already declares a CSP is left untouched.
`HtmlModulesPlugin.getCompilationHooks(compilation).alterAssetTags` lets plugins
return tag descriptors (`{ tag, attrs, children, injectTo, voidTag }`) that webpack
serializes and places at `head` / `body` / `head-prepend` / `body-prepend` — the
tag-injection half of Vite's `transformIndexHtml` (pair with `beforeEmit` for
string edits). Placement offsets are found with the pipeline's own HTML parser
(no regex). Runs before CSP so an injected inline `<script>`/`<style>` is hashed.
A MultiCompiler example pairing a modern `output.module` build with a classic build, using the `output.html` `alterAssetTags` hook to inject the classic bundle as `<script nomodule>` plus the standard Safari 10.1 nomodule fix. Shows differential serving without a new core option.
…example Add a `GenerateFaviconPlugin` to the existing html example: from `src/logo.png` it generates the modern icon set (`favicon.ico`, `apple-touch-icon`, 192/512 manifest icons) and a `manifest.webmanifest`, caches the whole set with `compilation.getCache()` keyed by the source hash, and injects the `<link>` / `<meta name="theme-color">` tags via the `output.html` `alterAssetTags` hook. Resizing to each size is the one step a real plugin does with an image library.
🦋 Changeset detectedLatest commit: 2655ee8 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 |
Replace the `<script>`/`<style>` matching regexps in the CSP and alterAssetTags test assertions with `indexOf`-based slicing, resolving the CodeQL `js/bad-tag-filter` alert (the `<script>` regexp didn't match upper-case tags). The extraction runs on webpack's own deterministic output, not untrusted input.
The hook is add-only — plugins push tag descriptors placed by `injectTo`, they don't mutate existing tags — so `injectTags` describes it more accurately than the html-webpack-plugin-borrowed `alterAssetTags`. Renames the hook, the `HtmlInjectTagsContext` type, the test case, the changeset and both examples.
`getCompilationHooks(compilation).alterTags` hands plugins the page's `<script>`/`<link>`/`<style>`/`<meta>` tags (webpack's own and any injected) as mutable descriptors; mutate `attrs` — add a `nonce`/`data-*`, switch `defer`↔`async` — or set `remove: true`, and webpack rewrites each changed tag in place. Complements the add-only `injectTags`. Tags are located with the pipeline's own parser (no regex) and only parsed when the hook is tapped; unchanged tags are left byte-for-byte.
…lEmitted `beforeEmit` is a string transform, not an emit, so `alterHtml` says what it does (and pairs with `alterTags`); `htmlEmitted` names the post-finalization event and avoids colliding with the Compiler's own `afterEmit`. The four hooks now read injectTags → alterTags → alterHtml → htmlEmitted. Updates the hook test and the pending changeset that introduced them.
`transform` is the plainer word for "change this and hand it back"; `alter` only came from mirroring html-webpack-plugin's `alterAssetTags` and isn't a webpack convention. The hooks now read injectTags → transformTags → transformHtml → htmlEmitted. Renames the hooks, their context types, the test case and the two changesets.
Add an `injectTo` field to the transformTags mutable descriptors so a plugin can move an existing tag between `<head>` and `<body>` (honoring the `*-prepend` variants), matching html-webpack-plugin's alterAssetTagGroups. Add html-csp and html-transform-tags examples.
…ures # Conflicts: # lib/html/HtmlModulesPlugin.js
|
This PR is packaged and the instant preview is available (7121604). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@7121604
yarn add -D webpack@https://pkg.pr.new/webpack@7121604
pnpm add -D webpack@https://pkg.pr.new/webpack@7121604 |
Merging this PR will improve performance by 71.66%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Memory | benchmark "css-modules", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
339.1 KB | 197.5 KB | +71.66% |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing feat/html-output-features (2655ee8) with main (828a65c)
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21487 +/- ##
==========================================
+ Coverage 93.50% 93.53% +0.02%
==========================================
Files 619 619
Lines 72956 73195 +239
Branches 21000 21098 +98
==========================================
+ Hits 68221 68462 +241
+ Misses 4735 4733 -2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merge the injectTags, transformTags and CSP stages onto one SourceProcessor pass: collectHtml gathers the page's tags, anchors and inline bodies once, the hooks act on that shared model, and renderHtml applies every edit (placement, attribute rewrites, moves, removals, CSP hashes/nonce/meta) in one right-to-left pass. Injected and existing tags are handled uniformly (a tag with a span vs. without), so an injected inline script is still hashed and an author CSP still suppresses the generated one.
|
The Generated by Claude Code |
The lazyCompilationBackend suite relies on jest's @sinonjs/fake-timers, which Bun can't install (setTimeout isn't fakeable), and the internalSerializables generator comparison formats with prettier, which trips Bun's module builtin. Guard both with the repo's process.versions.bun skip pattern so the runtimes (bun) job is green; they still run on Node.
|
Went ahead and made the Generated by Claude Code |
…ranch The HTML parser always synthesizes an implied <head>/<body>, so the 'missing region' fallbacks in renderHtml (prefix/suffix wrapping and the no-head CSP-meta path) were dead code; remove them and place every tag/meta at the always-present anchors. Add a unit case for a non-CSP <meta> under `output.html.csp` (the metaIsCsp false branch). Also reword a test comment to satisfy cspell.
Summary
Extends the experimental native HTML support (
experiments.html) with output features the html-webpack-plugin ecosystem and other bundlers provide, so they no longer need extra plugins:output.html.favicon— per-icon link attributes (sizes/media/color/type/crossorigin) and arrays of icons under onerel(e.g. severalsizes, light/darkmedia).output.html.manifest— generate a web app manifest from a path string or an object; itsicons/screenshotssrcare emitted as hashed assets by routing adata:application/manifest+jsonURL to the existingasset/webmanifestpipeline (new default mimetype rule).output.html.csp— inject a<meta http-equiv="Content-Security-Policy">with sha256/384/512 hashes of every inline<script>/<style>plus an optional nonce, over a strict baseline; located with the pipeline's own HTML parser (no regex).alterAssetTagscompilation hook — plugins return tag descriptors ({ tag, attrs, children, injectTo, voidTag }) that webpack serializes and places athead/body/*-prepend; the structured tag-injection surface the ecosystem relies on. Runs before CSP so injected inline tags are hashed.Also adds two examples: module/nomodule differential serving (MultiCompiler + the hook + the Safari 10.1 fix), and cached favicon-set +
manifest.webmanifestgeneration viacompilation.getCache().What kind of change does this PR introduce?
feat
Did you add tests for your changes?
Yes —
test/configCases/html/output-html-favicon,output-html-manifest,output-html-cspandoutput-html-alter-asset-tags.Does this PR introduce a breaking change?
No.
If relevant, what needs to be documented once your changes are merged or what have you already documented?
The new
output.html.favicon/manifest/cspoptions and thealterAssetTagshook need docs on the HTML /experimentspage; two runnable examples are included underexamples/.Use of AI
AI (Claude) was used to help implement and test these changes; all code, schemas and tests were reviewed and verified by the author, and the generated files (
types.d.ts,declarations/,schemas/**/*.check.js) were produced byyarn fix:special.🤖 Generated with Claude Code
Generated by Claude Code