Skip to content

feat: add NEXT_HASH_SALT env var for content-hash filename salting#91871

Merged
sokra merged 22 commits into
canaryfrom
sokra/hash-salt
Apr 1, 2026
Merged

feat: add NEXT_HASH_SALT env var for content-hash filename salting#91871
sokra merged 22 commits into
canaryfrom
sokra/hash-salt

Conversation

@sokra
Copy link
Copy Markdown
Member

@sokra sokra commented Mar 24, 2026

What?

Adds a NEXT_HASH_SALT environment variable and a experimental.outputHashSalt config option that mix a user-supplied string into every content-addressed hash used to generate chunk filenames and static asset filenames. This works for both Webpack and Turbopack.

When both are set, the values are concatenated (outputHashSalt + NEXT_HASH_SALT), so a per-project salt can be baked into next.config.js while a per-deployment salt is injected at build time via the environment variable.

Why?

Content-addressed filenames (e.g. chunk.abc123.js) are derived from file content, so they only change when the content changes. There are deployment scenarios where you need to force all filenames to rotate — for example after a CDN misconfiguration has poisoned caches for a particular hash space — without actually changing source code. A stable, opt-in salt lets operators do this without touching application code.

Some customers prefer the config-file approach (turbopack.outputHashSalt) over environment variables, so both are supported.

How?

Webpack already has output.hashSalt in its config. We simply forward NEXT_HASH_SALT to that option.

Turbopack required threading the value through several layers:

  1. The effective hash salt is computed once in assignDefaultsAndValidate as config.turbopackHashSalt = (turbopack.outputHashSalt ?? '') + (NEXT_HASH_SALT ?? '') and stored on NextConfigComplete. Both turbopackBuild (production) and createHotReloaderTurbopack (dev) read from this single field.
  2. ProjectOptions.hash_salt receives the pre-computed salt.
  3. Project stores the salt and passes it into the three chunking context option structs (ClientChunkingContextOptions, ServerChunkingContextOptions, EdgeChunkingContextOptions).
  4. Both BrowserChunkingContext and NodeJsChunkingContext gain a hash_salt: RcStr field.
  5. A new deterministic_hash_with_salt(salt, input, algorithm) function in turbo-tasks-hash writes the salt bytes first, then the content bytes, into a single hasher — one pass, no hash-of-hash composition.
  6. A matching content_hash_with_salt method is added to FileContent and AssetContent.
  7. ChunkingContext::asset_path is changed to accept Vc<AssetContent> (instead of a pre-computed Vc<RcStr>) so the chunking context can choose the correct hash path itself. StaticOutputAsset::path simplifies accordingly.

Without NEXT_HASH_SALT and without turbopack.outputHashSalt set, behaviour is identical to before — no hash change, no performance impact.

e2e test (test/production/app-dir/hash-salt/) verifies:

  • Two builds with the same salt produce identical chunk and static asset filenames.
  • A build with a different salt produces different filenames.
  • turbopack.outputHashSalt (config) changes filenames vs no salt.
  • Combined config + env salt differs from either alone.
  • Runs for both Turbopack and Webpack.

@nextjs-bot nextjs-bot added created-by: Turbopack team PRs by the Turbopack team. tests Turbopack Related to Turbopack with Next.js. type: next labels Mar 24, 2026
Comment thread turbopack/crates/turbo-tasks-fs/src/lib.rs Outdated
Comment thread turbopack/crates/turbo-tasks-hash/src/lib.rs Outdated
Comment thread turbopack/crates/turbo-tasks-fs/src/lib.rs Outdated
@nextjs-bot
Copy link
Copy Markdown
Contributor

nextjs-bot commented Mar 24, 2026

Tests Passed

Comment thread crates/next-api/src/project.rs Outdated
Comment thread test/production/app-dir/hash-salt/hash-salt.test.ts
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 24, 2026

Merging this PR will not alter performance

✅ 17 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing sokra/hash-salt (dccd88c) with canary (f7de136)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@nextjs-bot
Copy link
Copy Markdown
Contributor

nextjs-bot commented Mar 24, 2026

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms ▁▁▁█▇
Cold (Ready in log) 440ms 440ms ▁▁▁█▇
Cold (First Request) 1.136s 1.147s ▁▁▁█▆
Warm (Listen) 456ms 457ms ▁▁▁█▇
Warm (Ready in log) 444ms 446ms ▁▁▁█▇
Warm (First Request) 346ms 348ms ▁▁▁█▆
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms ▁▁▁▁█
Cold (Ready in log) 440ms 440ms ▁▁▂▁█
Cold (First Request) 1.963s 1.986s ▁▃▃▂█
Warm (Listen) 456ms 455ms ▁▁▁▁█
Warm (Ready in log) 440ms 439ms ▁▁▁▁█
Warm (First Request) 1.971s 1.971s ▁▃▂▂█

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 3.743s 3.763s ▁▁▁██
Cached Build 3.767s 3.765s ▁▁▁█▇
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.429s 14.463s ▁▁▂▁█
Cached Build 14.661s 14.551s ▁▁▂▁█
node_modules Size 485 MB 485 MB ▁▁▁▁█
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
00iotnnw2_xfy.js gzip 70.8 kB N/A -
02fkg8wfh0iju.js gzip 9.19 kB N/A -
050zwt5xh_0tx.js gzip 10.4 kB N/A -
06rvbj82bhyo0.js gzip 13 kB N/A -
087fzjd-gvlzv.js gzip 450 B N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0ppxcl_z43mad.js gzip 8.52 kB N/A -
0z_g5xb06rni8.js gzip 155 B N/A -
19oha6-znmkcv.js gzip 8.55 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1r404v4-3v4_u.js gzip 168 B N/A -
1tfdnbt58l2-x.js gzip 159 B N/A -
1veummv7rqv69.js gzip 153 B N/A -
2_5rjb7lqxntf.js gzip 221 B 221 B
219prxwxgaalc.js gzip 7.61 kB N/A -
26elcgxnn9zjd.js gzip 8.52 kB N/A -
2900hudr6gvm0.js gzip 2.28 kB N/A -
2jo4n-moj718i.js gzip 157 B N/A -
2lv2js3kmdeho.js gzip 8.48 kB N/A -
2rehygrd36hqv.js gzip 8.58 kB N/A -
2rvyazlx9_6iq.js gzip 157 B N/A -
2srwswih0m9_h.js gzip 13.3 kB N/A -
2vzdej-0msygw.js gzip 155 B N/A -
2x28o5srzht32.js gzip 157 B N/A -
3-p9p9mheqhzx.js gzip 8.55 kB N/A -
31030bryqpolg.js gzip 8.53 kB N/A -
31dx5nmrzzuy7.js gzip 225 B N/A -
3925v09gtu-5k.js gzip 49 kB N/A -
39x4zj5mjb4d_.js gzip 9.77 kB N/A -
3fc08f71xefg1.js gzip 155 B N/A -
3jic6i9bseep6.js gzip 65.7 kB N/A -
3k-48b78ys_vy.js gzip 10.1 kB N/A -
3lnfio3cnq7f0.js gzip 156 B N/A -
3m7-5rfj0avoz.js gzip 12.9 kB N/A -
3u6fn2mw1di--.js gzip 155 B N/A -
3uqce_6sa526g.js gzip 8.47 kB N/A -
3yurjqk-sjs3y.js gzip 1.46 kB N/A -
40ybjx9c192n0.js gzip 13.8 kB N/A -
41-_01ln_2zkh.js gzip 155 B N/A -
421vzwdt9j1b_.js gzip 5.62 kB N/A -
4438caaqile4_.js gzip 161 B N/A -
turbopack-03..n4nb.js gzip 4.18 kB N/A -
turbopack-04..ikcf.js gzip 4.15 kB N/A -
turbopack-04..alx1.js gzip 4.18 kB N/A -
turbopack-0j..dga0.js gzip 4.18 kB N/A -
turbopack-0l.._n9x.js gzip 4.18 kB N/A -
turbopack-0o..wy29.js gzip 4.18 kB N/A -
turbopack-12..pglf.js gzip 4.17 kB N/A -
turbopack-1j..o_c0.js gzip 4.19 kB N/A -
turbopack-1z..q6p8.js gzip 4.17 kB N/A -
turbopack-2-..qsnb.js gzip 4.18 kB N/A -
turbopack-26..mq9v.js gzip 4.17 kB N/A -
turbopack-2g..ahbp.js gzip 4.18 kB N/A -
turbopack-2w..j_8n.js gzip 4.18 kB N/A -
turbopack-44..dyyi.js gzip 4.17 kB N/A -
03dgzoo-qf3sm.js gzip N/A 9.19 kB -
05tx5f25dlivn.js gzip N/A 8.53 kB -
076qdhguprj3g.js gzip N/A 157 B -
07x7y9orroch_.js gzip N/A 154 B -
0c7ez6p2qc57f.js gzip N/A 5.62 kB -
0duvj3qk5pvgn.js gzip N/A 13.8 kB -
0m-34rm9w_wpm.js gzip N/A 7.6 kB -
0qnwuk92m8i7o.js gzip N/A 10.4 kB -
0r4wrn6n0ue2m.js gzip N/A 8.55 kB -
0rp0fodtbt_6m.js gzip N/A 8.52 kB -
0sfck-km4dl1k.js gzip N/A 8.47 kB -
0x0xuhmxzwkp8.js gzip N/A 8.47 kB -
1-wdvgxnzicj7.js gzip N/A 1.46 kB -
11u6nxujb2eg4.js gzip N/A 450 B -
1jv-o1_s-zmua.js gzip N/A 49 kB -
1qr4jxytqrquk.js gzip N/A 167 B -
1tawq-_7w5wnt.js gzip N/A 156 B -
1vhsa1istl8vk.js gzip N/A 155 B -
1vtzm80ig7ons.js gzip N/A 154 B -
25m7mqgt86ixn.js gzip N/A 160 B -
2c_83vh6ra6-m.js gzip N/A 152 B -
2e4t24z72pt2f.js gzip N/A 70.8 kB -
2hgmo8vho4vf0.js gzip N/A 152 B -
2k9ax08cjl2id.js gzip N/A 12.9 kB -
2lms6k76q5-6m.js gzip N/A 13.3 kB -
2luekerf2irq-.js gzip N/A 65.7 kB -
2qx4twi9i3xus.js gzip N/A 2.28 kB -
2rpa9-yryz91a.js gzip N/A 155 B -
2srnqic6tvxxd.js gzip N/A 8.52 kB -
30l7m4nayp73a.js gzip N/A 8.55 kB -
38rr7d3kfutni.js gzip N/A 13 kB -
3cka21fecurt7.js gzip N/A 157 B -
3h_ecpiaatwgc.js gzip N/A 10.1 kB -
3huq6k2x99obf.js gzip N/A 154 B -
3ity0aahajapd.js gzip N/A 225 B -
3wrhpuc-j1aw9.js gzip N/A 9.77 kB -
42q80l5qx8xoe.js gzip N/A 155 B -
43mlw9dy_8f02.js gzip N/A 8.58 kB -
turbopack-0j..6ao-.js gzip N/A 4.17 kB -
turbopack-19..y-ui.js gzip N/A 4.16 kB -
turbopack-1g..8mcq.js gzip N/A 4.18 kB -
turbopack-1g..srop.js gzip N/A 4.17 kB -
turbopack-1l..ffik.js gzip N/A 4.17 kB -
turbopack-2e..h56q.js gzip N/A 4.17 kB -
turbopack-2i..x_ek.js gzip N/A 4.19 kB -
turbopack-35..wsrp.js gzip N/A 4.17 kB -
turbopack-37..qdma.js gzip N/A 4.18 kB -
turbopack-39..9fws.js gzip N/A 4.18 kB -
turbopack-3a..z3tb.js gzip N/A 4.17 kB -
turbopack-3c..fl01.js gzip N/A 4.17 kB -
turbopack-3g..off8.js gzip N/A 4.18 kB -
turbopack-3q..1pmk.js gzip N/A 4.17 kB -
Total 464 kB 464 kB ✅ -49 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 715 B 712 B
Total 715 B 712 B ✅ -3 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 436 B 430 B 🟢 6 B (-1%)
Total 436 B 430 B ✅ -6 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.54 kB N/A -
6280-HASH.js gzip 60.7 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.59 kB N/A -
e8aec2e4-HASH.js gzip 62.8 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 255 B 254 B
main-HASH.js gzip 39.3 kB 39.2 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.59 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.55 kB -
6948ada0-HASH.js gzip N/A 62.8 kB -
9544-HASH.js gzip N/A 61.4 kB -
Total 235 kB 235 kB ⚠️ +649 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.51 kB 2.51 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.98 kB 7.98 kB ✅ -1 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 125 kB
page.js gzip 270 kB 270 kB
Total 396 kB 396 kB ⚠️ +165 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 618 B 618 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 43.9 kB 44 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.6 kB 45.6 kB ⚠️ +68 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 4.35 MB 4.34 MB
index.pack gzip 110 kB 110 kB
index.pack.old gzip 110 kB 110 kB
Total 4.57 MB 4.57 MB ✅ -2.42 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 336 kB 336 kB
app-page-exp..prod.js gzip 186 kB 186 kB
app-page-tur...dev.js gzip 335 kB 335 kB
app-page-tur..prod.js gzip 185 kB 185 kB
app-page-tur...dev.js gzip 332 kB 332 kB
app-page-tur..prod.js gzip 183 kB 183 kB
app-page.run...dev.js gzip 332 kB 332 kB
app-page.run..prod.js gzip 184 kB 184 kB
app-route-ex...dev.js gzip 76.3 kB 76.3 kB
app-route-ex..prod.js gzip 51.9 kB 51.9 kB
app-route-tu...dev.js gzip 76.3 kB 76.3 kB
app-route-tu..prod.js gzip 51.9 kB 51.9 kB
app-route-tu...dev.js gzip 75.9 kB 75.9 kB
app-route-tu..prod.js gzip 51.7 kB 51.7 kB
app-route.ru...dev.js gzip 75.9 kB 75.9 kB
app-route.ru..prod.js gzip 51.7 kB 51.7 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.5 kB 43.5 kB
pages-api-tu..prod.js gzip 33.1 kB 33.1 kB
pages-api.ru...dev.js gzip 43.5 kB 43.5 kB
pages-api.ru..prod.js gzip 33.1 kB 33.1 kB
pages-turbo....dev.js gzip 52.9 kB 52.9 kB
pages-turbo...prod.js gzip 38.7 kB 38.7 kB
pages.runtim...dev.js gzip 52.9 kB 52.9 kB
pages.runtim..prod.js gzip 38.7 kB 38.7 kB
server.runti..prod.js gzip 62.5 kB 62.5 kB
Total 2.99 MB 2.99 MB ⚠️ +2 B
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/dccd88cea986b98a4bacec9103f8abf62fc9ee14/next

@sokra sokra force-pushed the sokra/hash-salt branch from 2e66515 to deb9451 Compare March 25, 2026 07:40
@sokra sokra requested a review from mischnic March 26, 2026 09:50
Comment thread crates/next-api/src/project.rs Outdated
Comment thread packages/next/src/build/swc/generated-native.d.ts Outdated
Comment thread packages/next/src/build/turbopack-build/impl.ts Outdated
Comment thread packages/next/src/build/webpack-config.ts Outdated
Comment thread packages/next/src/server/dev/hot-reloader-turbopack.ts Outdated
@sokra sokra force-pushed the sokra/hash-salt branch from c5d56c8 to 3efa656 Compare March 27, 2026 00:47
@sokra sokra marked this pull request as ready for review March 27, 2026 09:54
@sokra sokra force-pushed the sokra/hash-salt branch from 650c567 to 73f1634 Compare March 27, 2026 17:46
@sokra sokra requested a review from lukesandberg March 27, 2026 21:23
Copy link
Copy Markdown
Contributor

@lukesandberg lukesandberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an experimental config option

we can still default to the environment variable but for some customers like faire they would prefer a config option

or just a turbopack option turbopack.outputHashSalt

@nextjs-bot nextjs-bot added the Documentation Related to Next.js' official documentation. label Mar 27, 2026
@lukesandberg lukesandberg self-requested a review March 27, 2026 22:14
Comment thread docs/01-app/03-api-reference/05-config/01-next-config-js/turbopack.mdx Outdated
Comment thread packages/next/src/build/turbopack-build/impl.ts Outdated
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 27, 2026

Notifying the following users due to files changed in this PR based on this repo's notify modifiers:

@timneutkens, @ijjk, @shuding, @huozhi:

packages/next/src/server/config.ts

sokra and others added 20 commits April 1, 2026 07:57
…tterns

- Add `hash_with_salt(hash, salt) -> String` to `turbo-tasks-hash` so the
  logic lives once and both chunking contexts import it instead of each
  defining an identical private `apply_hash_salt` helper.
- Replace the two-step `let salted_hash; let x = if let Some(salt) …`
  pattern with a cleaner `match .as_deref()` form, and consolidate the
  length-trimming into the same arm so the intent is clear.
- Merge the two-describe / two-`nextTestSetup` test into a single describe
  that runs three builds in one `beforeAll`, testing both invariants with
  one less app instance and one less full build.

Co-Authored-By: Claude <noreply@anthropic.com>
…ings

The previous approach fed the existing hash string back through a second
hasher (hashing a hash), which is not a sound construction. The salt must
be mixed with the original content bytes in a single pass.

New approach:
- Add `deterministic_hash_with_salt(salt, input, algorithm)` to
  `turbo-tasks-hash` that writes salt bytes first, then the content, into
  one hasher — same algorithm dispatch as `deterministic_hash`.
- Add `FileContent::content_hash_with_salt` and
  `AssetContent::content_hash_with_salt` that delegate to it, matching
  the existing `content_hash` API surface.
- Change `ChunkingContext::asset_path` to accept `Vc<AssetContent>`
  instead of a pre-computed `Vc<RcStr>`, so the chunking context can
  choose between `content_hash` and `content_hash_with_salt` based on
  its own `hash_salt` field.
- `StaticOutputAsset::path` simplifies to a direct pass-through with no
  hash pre-computation.
- Both chunking context `asset_path` and the browser `chunk_path`
  implementations call `content_hash_with_salt` when a salt is set and
  `content_hash` otherwise — no post-processing of hash strings.

Co-Authored-By: Claude <noreply@anthropic.com>
- Remove separate deterministic_hash_with_salt; salt is now the first
  (always-present) parameter of deterministic_hash — empty string = no salt
- Remove content_hash_with_salt from FileContent/AssetContent/Asset; merged
  into content_hash which now takes Vc<RcStr> salt as first parameter
- Remove hash_salt: Option<RcStr> from BrowserChunkingContext and
  NodeJsChunkingContext; replaced with RcStr (empty = no salt), eliminating
  the if/else branches at every call site
- Project::hash_salt() now returns Vc<RcStr> (unwraps the option); option
  struct fields in next-core contexts updated accordingly
- Fix missing hash_salt field in next-build-test ProjectOptions initializer
  (was causing the rust-check-clippy CI failure)
- Add image import to hash-salt test fixture and verify image filenames also
  change when the salt changes

Co-Authored-By: Claude <noreply@anthropic.com>
- Extract feed() helper in deterministic_hash to eliminate 4-line pattern
  repeated 7 times (write salt, write input, finish, encode)
- Add no_hash_salt() helper in turbopack-core::asset to replace the
  verbose Vc::cell(RcStr::default()) pattern at call sites that don't
  control the hash salt; 5 callers updated
- Extract buildWithSalt() helper in the test to eliminate the duplicated
  build/collect/clean sequence; also moves next.clean() inside the helper
  so each build is self-contained

Co-Authored-By: Claude <noreply@anthropic.com>
- Remove explicit deref `&*salt.await?` → `&salt.await?` to satisfy
  clippy::explicit_auto_deref (-D warnings) in turbo-tasks-fs
- Replace the malformed 1×1 PNG fixture (which sharp rejects with
  'libspng read error' during webpack blur-placeholder generation) with
  a valid 8×8 solid-color PNG

Co-Authored-By: Claude <noreply@anthropic.com>
Two call sites in browser and node chunking contexts were not updated
from Xxh3Hash128Base40 to Xxh3Hash128Base38 during the rebase onto the
base38-rename commit, causing compile errors in all Rust CI jobs.
- Change ProjectOptions/Project.hash_salt from Option<RcStr> to RcStr;
  callers now pass '' when no salt is set, avoiding unwrap_or_default()
- Mirror change in NapiProjectOptions and generated-native.d.ts so the
  TypeScript API matches: hashSalt is always provided, never undefined
- Use '' instead of undefined in all JS/TS call sites so the napi binding
  receives the correct type
- Pass NEXT_HASH_SALT into the next-image-loader and mix it into the
  content buffer used for hash computation, so static image filenames
  change with the salt (same behaviour as chunk filenames)

Co-Authored-By: Claude <noreply@anthropic.com>
…Salt to test

Webpack's schema requires output.hashSalt to be a non-empty string (minLength 1),
so passing '' when the env var is absent causes a ValidationError that breaks
every webpack build. Omit the option entirely when no salt is configured.

Also add the now-required hashSalt field to the two createProject calls in
next-rs-api.test.ts that were missing it after the Option→required change.

Co-Authored-By: Claude <noreply@anthropic.com>
…nv var

Customers who cannot use environment variables can now set a hash salt
via next.config.js turbopack.outputHashSalt. When both are set, the
values are concatenated (config + env) so each can be set independently
without conflict.

Co-Authored-By: Claude <noreply@anthropic.com>
…ix tests, fix doc version

- Add turbopackHashSalt to NextConfigComplete, computed once in
  assignDefaultsAndValidate (outputHashSalt + NEXT_HASH_SALT) so build
  and dev share identical logic without duplication
- Replace multiple nextTestSetup instances (which violated the singleton
  constraint) with a single instance using a fixture next.config.js that
  reads OUTPUT_HASH_SALT_CONFIG env var, allowing sequential builds with
  different salts in one describe block
- Move turbopack.outputHashSalt version history entry to 16.3.0 at the
  top of the table per review comment

Co-Authored-By: Claude <noreply@anthropic.com>
turbopack.outputHashSalt is a Turbopack-only config option; webpack
ignores it, so both builds produce identical filenames and the
not.toEqual assertions fail. Guard the beforeAll builds and each
assertion with isTurbopack so they are no-ops under webpack.

Co-Authored-By: Claude <noreply@anthropic.com>
…bpack

- Rename config option from turbopack.outputHashSalt to
  experimental.outputHashSalt so it applies to both Webpack and Turbopack
- Update webpack-config.ts to use config.turbopackHashSalt (which now
  combines experimental.outputHashSalt + NEXT_HASH_SALT) for both the
  output.hashSalt webpack option and the metadata image loader hashSalt
- Remove isTurbopack guards from the outputHashSalt tests since the
  config option now works with both bundlers
- Add dedicated outputHashSalt.mdx doc page under experimental config
- Remove outputHashSalt from the turbopack.mdx options table and section

Co-Authored-By: Claude <noreply@anthropic.com>
- #2: turbopack-analyze now combines turbopack.outputHashSalt + NEXT_HASH_SALT (was env-only)
- #5: asset_hashes_manifest uses salted hash so recorded hash matches filename derivation;
  expose Project::hash_salt as pub(crate) to allow access from sibling modules
- #7: add globals.css fixture and CSS filename assertions to hash-salt test
- #10: mark no_hash_salt() with #[turbo_tasks::function] for guaranteed deduplication
- #12: add hash_salt_vc() helper to BrowserChunkingContext and NodeJsChunkingContext,
  use it in chunk_path to remove the intermediate await
- #14: turbopack.outputHashSalt now feeds into turbopackHashSalt (and thus webpack's
  output.hashSalt), add outputHashSalt to TurbopackOptions type and zod schema,
  update docs to show turbopack.outputHashSalt as the primary location

Co-Authored-By: Claude <noreply@anthropic.com>
- #1: turbopack-analyze now uses config.hashSalt (was recomputing from parts,
  missing experimental.outputHashSalt)
- #2: add same-salt reproducibility tests for images and CSS; add a new
  turbopack.outputHashSalt describe block with TURBOPACK_HASH_SALT_CONFIG
- #3: next.config.js fixture exposes turbopack.outputHashSalt via env var
- #4: asset_path in BrowserChunkingContext and NodeJsChunkingContext changed to
  self: Vc<Self> receiver so it can call self.hash_salt_vc() consistently,
  matching chunk_path
- #5: rename NextConfigComplete.turbopackHashSalt → hashSalt (bundler-agnostic
  name; all call sites updated)
- #6: remove version:experimental from docs frontmatter (turbopack.outputHashSalt
  is stable); deprecate experimental.outputHashSalt alias in the note

Co-Authored-By: Claude <noreply@anthropic.com>
- Remove outputHashSalt from TurbopackOptions interface and zod schema
- Remove turbopack?.outputHashSalt from the hashSalt computation in config.ts
- Docs rewritten to show experimental.outputHashSalt as the primary option
- Test fixture next.config.js drops the turbopack block
- Remove turbopack.outputHashSalt describe block from tests

Co-Authored-By: Claude <noreply@anthropic.com>
Turbopack emits CSS inside .next/static/chunks/ rather than
.next/static/css/, so searching only the css/ subdirectory finds
nothing. Switch to recursiveReadDir over the full .next/static/ tree
and strip the directory prefix, keeping only basenames for comparison.

Also import recursiveReadDir which is already used in other production
tests for the same reason.

Co-Authored-By: Claude <noreply@anthropic.com>
Per review feedback: store the hash salt as a ResolvedVc constructed by
the project rather than a plain RcStr. This avoids allocating a new Vc
cell on every hash_salt_vc() call and makes it explicit that the value
is owned by the project's turbo-tasks graph.

- BrowserChunkingContext.hash_salt: RcStr → ResolvedVc<RcStr>
- NodeJsChunkingContext.hash_salt: RcStr → ResolvedVc<RcStr>
- hash_salt_vc() returns *self.hash_salt instead of Vc::cell(clone)
- Builder .hash_salt() takes ResolvedVc<RcStr> directly
- Option structs (Client/Server/Edge) updated from Vc to ResolvedVc
- project.rs resolves self.hash_salt() once via .to_resolved().await?

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Each beforeAll runs 3-4 sequential next builds (~35-45s each in CI),
which can exceed the default 120s Jest hook timeout.

Co-Authored-By: Claude <noreply@anthropic.com>
@sokra sokra force-pushed the sokra/hash-salt branch from bb01fed to dccd88c Compare April 1, 2026 08:00
@sokra sokra merged commit 3e01588 into canary Apr 1, 2026
408 of 418 checks passed
@sokra sokra deleted the sokra/hash-salt branch April 1, 2026 20:03
wbinnssmith pushed a commit that referenced this pull request Apr 2, 2026
…91871)

### What?

Adds a `NEXT_HASH_SALT` environment variable **and** a
`experimental.outputHashSalt` config option that mix a user-supplied
string into every content-addressed hash used to generate chunk
filenames and static asset filenames. This works for both Webpack and
Turbopack.

When both are set, the values are concatenated (`outputHashSalt +
NEXT_HASH_SALT`), so a per-project salt can be baked into
`next.config.js` while a per-deployment salt is injected at build time
via the environment variable.

### Why?

Content-addressed filenames (e.g. `chunk.abc123.js`) are derived from
file content, so they only change when the content changes. There are
deployment scenarios where you need to force all filenames to rotate —
for example after a CDN misconfiguration has poisoned caches for a
particular hash space — without actually changing source code. A stable,
opt-in salt lets operators do this without touching application code.

Some customers prefer the config-file approach
(`turbopack.outputHashSalt`) over environment variables, so both are
supported.

### How?

**Webpack** already has `output.hashSalt` in its config. We simply
forward `NEXT_HASH_SALT` to that option.

**Turbopack** required threading the value through several layers:

1. The effective hash salt is computed once in
`assignDefaultsAndValidate` as `config.turbopackHashSalt =
(turbopack.outputHashSalt ?? '') + (NEXT_HASH_SALT ?? '')` and stored on
`NextConfigComplete`. Both `turbopackBuild` (production) and
`createHotReloaderTurbopack` (dev) read from this single field.
2. `ProjectOptions.hash_salt` receives the pre-computed salt.
3. `Project` stores the salt and passes it into the three chunking
context option structs (`ClientChunkingContextOptions`,
`ServerChunkingContextOptions`, `EdgeChunkingContextOptions`).
4. Both `BrowserChunkingContext` and `NodeJsChunkingContext` gain a
`hash_salt: RcStr` field.
5. A new `deterministic_hash_with_salt(salt, input, algorithm)` function
in `turbo-tasks-hash` writes the salt bytes first, then the content
bytes, into a single hasher — one pass, no hash-of-hash composition.
6. A matching `content_hash_with_salt` method is added to `FileContent`
and `AssetContent`.
7. `ChunkingContext::asset_path` is changed to accept `Vc<AssetContent>`
(instead of a pre-computed `Vc<RcStr>`) so the chunking context can
choose the correct hash path itself. `StaticOutputAsset::path`
simplifies accordingly.

Without `NEXT_HASH_SALT` and without `turbopack.outputHashSalt` set,
behaviour is identical to before — no hash change, no performance
impact.

**e2e test** (`test/production/app-dir/hash-salt/`) verifies:
- Two builds with the same salt produce identical chunk and static asset
filenames.
- A build with a different salt produces different filenames.
- `turbopack.outputHashSalt` (config) changes filenames vs no salt.
- Combined config + env salt differs from either alone.
- Runs for both Turbopack and Webpack.

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Luke Sandberg <lukesandberg@users.noreply.github.com>
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Apr 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

created-by: Turbopack team PRs by the Turbopack team. Documentation Related to Next.js' official documentation. locked tests Turbopack Related to Turbopack with Next.js. type: next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants