feat: add NEXT_HASH_SALT env var for content-hash filename salting#91871
Conversation
Tests Passed |
Merging this PR will not alter performance
Comparing Footnotes
|
Stats from current PR✅ No significant changes detected📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles
Server Middleware
Build DetailsBuild Manifests
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📎 Tarball URL |
lukesandberg
left a comment
There was a problem hiding this comment.
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
|
Notifying the following users due to files changed in this PR based on this repo's notify modifiers: @timneutkens, @ijjk, @shuding, @huozhi: |
…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>
…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>
What?
Adds a
NEXT_HASH_SALTenvironment variable and aexperimental.outputHashSaltconfig 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 intonext.config.jswhile 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.hashSaltin its config. We simply forwardNEXT_HASH_SALTto that option.Turbopack required threading the value through several layers:
assignDefaultsAndValidateasconfig.turbopackHashSalt = (turbopack.outputHashSalt ?? '') + (NEXT_HASH_SALT ?? '')and stored onNextConfigComplete. BothturbopackBuild(production) andcreateHotReloaderTurbopack(dev) read from this single field.ProjectOptions.hash_saltreceives the pre-computed salt.Projectstores the salt and passes it into the three chunking context option structs (ClientChunkingContextOptions,ServerChunkingContextOptions,EdgeChunkingContextOptions).BrowserChunkingContextandNodeJsChunkingContextgain ahash_salt: RcStrfield.deterministic_hash_with_salt(salt, input, algorithm)function inturbo-tasks-hashwrites the salt bytes first, then the content bytes, into a single hasher — one pass, no hash-of-hash composition.content_hash_with_saltmethod is added toFileContentandAssetContent.ChunkingContext::asset_pathis changed to acceptVc<AssetContent>(instead of a pre-computedVc<RcStr>) so the chunking context can choose the correct hash path itself.StaticOutputAsset::pathsimplifies accordingly.Without
NEXT_HASH_SALTand withoutturbopack.outputHashSaltset, behaviour is identical to before — no hash change, no performance impact.e2e test (
test/production/app-dir/hash-salt/) verifies:turbopack.outputHashSalt(config) changes filenames vs no salt.