Re-encode inline hash digests ([contenthash]/[chunkhash]/[fullhash]/[modulehash]) from full content#21267
Conversation
🦋 Changeset detectedLatest commit: 844a4a1 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 (7c81bde). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@7c81bde
yarn add -D webpack@https://pkg.pr.new/webpack@7c81bde
pnpm add -D webpack@https://pkg.pr.new/webpack@7c81bde |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21267 +/- ##
==========================================
+ Coverage 92.81% 92.84% +0.02%
==========================================
Files 592 592
Lines 64964 65022 +58
Branches 18135 18162 +27
==========================================
+ Hits 60299 60368 +69
+ Misses 4665 4654 -11
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:
|
Merging this PR will improve performance by ×2.2
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Memory | benchmark "many-chunks-commonjs", scenario '{"name":"mode-production","mode":"production"}' |
7.3 MB | 9.7 MB | -25.41% |
| ❌ | Memory | benchmark "many-modules-commonjs", scenario '{"name":"mode-production","mode":"production"}' |
7.7 MB | 10 MB | -22.83% |
| ⚡ | Memory | benchmark "lodash", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
850.9 KB | 127.6 KB | ×6.7 |
| ⚡ | Memory | benchmark "side-effects-reexport", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
860 KB | 130.9 KB | ×6.6 |
| ⚡ | Memory | benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
859.4 KB | 245.7 KB | ×3.5 |
| ⚡ | Memory | benchmark "devtool-eval", scenario '{"name":"mode-production","mode":"production"}' |
7.8 MB | 6.1 MB | +26.31% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing claude/css-hash-inline-digest-7806a1 (844a4a1) with main (eefaf44)
…:<digest>] Inline `[contenthash:<digest>]` previously threw under optimization.realContentHash because the plugin recomputed content hashes in output.hashDigest, ignoring the inline digest. Record the requested digest per hash during path interpolation and re-encode the recomputed real content hash in it, so the inline digest now works (and carries full content entropy) instead of being rejected.
…ntent Inline `[contenthash:<digest>]`/`[chunkhash:<digest>]` re-encoded the already-truncated stored hash, and in the dynamic chunk-filename runtime module the digest re-encode mangled the per-chunk map, dropping the hash entirely so dynamically-loaded chunks 404'd. Retain the full content digest per chunk (transient, recomputed each seal) and re-encode from it in both the static asset path and the per-chunk runtime map, so the value carries full entropy and the runtime URL matches the emitted filename. `[fullhash:<digest>]`/`[hash:<digest>]` in a dynamically-loaded chunk filename now throws a clear error, since it resolves to a runtime getFullHash() expression that cannot be re-encoded.
The base-N encoder dropped leading 0x00 bytes (whole digest collapsed to one BigInt), so two distinct digests could collide and a zero-heavy digest encoded far shorter than requested, making [contenthash:baseN:N] slices too short. Re- emit one leading alphabet[0] char per leading zero byte (and decode the inverse), matching the function's own documented contract. Digests without a leading zero byte are unchanged, so typical output hashes are unaffected.
…full content Inline [contenthash:<digest>]/[modulehash:<digest>] in module/asset contexts (asset-module filenames) re-encoded the already-truncated stored hash, so the digest carried no extra entropy and an inline length couldn't exceed output.hashDigestLength. Thread the full content digest through AssetGenerator into PathData (contentHashFull) and pass the full module hash (getModuleHash, not getRenderedModuleHash), so the static re-encode uses full entropy — matching the chunk-context behaviour added earlier.
[fullhash:<digest>]/[hash:<digest>] in a dynamically-loaded chunk filename previously threw, because the runtime builds it from getFullHash() — a runtime expression that cannot be re-encoded. When such a template is used, flag the GetChunkFilenameRuntimeModule as fullHash so it re-renders after hashing, and inline the re-encoded full compilation hash directly instead of getFullHash(). The inlined value is byte-identical to the statically emitted filename, so the chunk loads with full entropy (no 404). The length-only [fullhash:8] form is unchanged and still uses the getFullHash() runtime expression.
lint:types-test (tsc -p tsconfig.types.test.json) type-checks the test files; the new cases passed synthetic object literals that don't satisfy PathData / AssetInfo. Cast them with EXPECTED_ANY, matching the existing idiom in this file.
251d21e to
05f73cb
Compare
…imeModule A function chunkFilename routes chunks through addStaticUrl rather than the dynamic map, so its inline-digest handlers (chunkhash/contenthash) had no test. Add a config case that loads an async chunk through that path.
Types CoverageCoverage after merging claude/css-hash-inline-digest-7806a1 into main will be
Coverage Report |
Summary
Follow-up to #21259 (#21141). The inline hash digest feature re-encoded the already-truncated stored hash, so
[contenthash:<digest>]carried no extra entropy, threw underoptimization.realContentHash, and produced 404s for dynamically-loaded chunks (the runtime dropped the digest hash entirely). This re-encodes[contenthash]/[chunkhash]/[fullhash]/[modulehash]from the full content hash so the value is full-entropy and the runtime URL matches the emitted filename. Also makesRealContentHashPlugindigest-aware, supports[fullhash:<digest>]in dynamic chunk filenames (inlined after hashing), and fixes base-N digests to preserve leading zero bytes (a latent collision).What kind of change does this PR introduce?
feat (includes a base-N digest collision fix).
Did you add tests for your changes?
Yes —
test/hash-digest.unittest.js, additions totest/TemplatedPathPlugin.unittest.js, andtest/configCases/hash-length/{digest,digest-async-chunk,digest-asset-module,digest-fullhash-chunkname}. FullConfigTestCases+HotTestCases+ stats/unit suites pass.Does this PR introduce a breaking change?
Only output-value changes, no API breakage (signature changes are added optional params). base-N
output.hashDigest(e.g.base58) changes the filename for digests that start with a0x00byte (~0.4% per leading byte) — this fixes a real collision where two distinct digests encoded to the same string; migration is a one-time cache-bust. The inline[*:<digest>]syntax is from the unreleased #21259, so no published version changes behavior.If relevant, what needs to be documented once your changes are merged or what have you already documented?
docs.webpack.js.org: note that an inline
[..:<digest>]re-encodes from the full hash (full entropy, can exceedhashDigestLength), that the digit-only[..:N]form stays bound tohashDigestLength, and thatbase64is filename/ident-unsafe (usebase64url).Use of AI
Implemented with Claude Code (Anthropic). AI was used to analyze the existing hash/runtime paths, write the implementation and tests, and run the test suite and a CPU/memory benchmark (no measurable regression); all changes were reviewed before committing.
Generated by Claude Code