Respect NEXT_HASH_SALT for server side assetsHashes (#95738)

Also respect the hash salt for `assetsHashes`
This commit is contained in:
Niklas Mischkulnig
2026-07-15 11:28:06 +02:00
committed by GitHub
parent ca414736ba
commit b3480cfdd5
10 changed files with 155 additions and 40 deletions
+8 -3
View File
@@ -115,12 +115,15 @@ impl Asset for ServerNftJsonAsset {
)
.connect();
let hash_salt = this.project.next_config().output_hash_salt();
let mut server_output_assets = traced_modules_for_entries(
module_graph,
Modules::empty(),
self.entries(),
Some(self.ignores()),
None,
hash_salt,
)
.await?
.iter()
@@ -133,7 +136,7 @@ impl Asset for ServerNftJsonAsset {
.await?
.context("NFT module has no content")?
.content()
.hash(HashAlgorithm::Xxh3Hash128Hex)
.hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
.await?,
))
})
@@ -150,7 +153,7 @@ impl Asset for ServerNftJsonAsset {
.context("failed to compute relative path for server NFT JSON")?,
module_path
.read()
.hash(HashAlgorithm::Xxh3Hash128Hex)
.hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
.await?,
));
@@ -170,7 +173,9 @@ impl Asset for ServerNftJsonAsset {
base_dir
.get_relative_path_to(file)
.context("failed to compute relative path for server NFT JSON")?,
file.read().hash(HashAlgorithm::Xxh3Hash128Hex).await?,
file.read()
.hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
.await?,
))
}
}
+7 -3
View File
@@ -67,6 +67,7 @@ pub async fn trace_endpoint(
async {
let project_path = project.project_path().owned().await?;
let next_config = project.next_config();
let hash_salt = next_config.output_hash_salt();
let output_file_tracing_includes = next_config
.output_file_tracing_includes(project_path.clone())
@@ -83,10 +84,11 @@ pub async fn trace_endpoint(
.await?
.map(|v| *v),
Some(next_config.config_file_path(project_path.clone())),
hash_salt,
)
.await?;
let module_data = traced_module_data_for_graph(*module_graph, traced_entries)
let module_data = traced_module_data_for_graph(*module_graph, traced_entries, hash_salt)
.to_resolved()
.await?;
let module_paths = module_data.await?.idents;
@@ -271,10 +273,11 @@ pub async fn traced_modules_for_entries(
traced_entries: Vc<Modules>,
exclude_glob: Option<Vc<Glob>>,
forbidden_path: Option<Vc<FileSystemPath>>,
hash_salt: Vc<RcStr>,
) -> Result<Vc<Modules>> {
let exclude_glob_and_module_idents = if let Some(exclude_glob) = exclude_glob {
let exclude_glob = exclude_glob.await?;
let data = traced_module_data_for_graph(module_graph, traced_entries).await?;
let data = traced_module_data_for_graph(module_graph, traced_entries, hash_salt).await?;
Some((exclude_glob, data.idents.await?))
} else {
None
@@ -379,6 +382,7 @@ pub struct TracedModuleData {
pub async fn traced_module_data_for_graph(
module_graph: Vc<ModuleGraph>,
traced_entries: Vc<Modules>,
hash_salt: Vc<RcStr>,
) -> Result<Vc<TracedModuleData>> {
// This function is very similar to traced_modules_for_entries, but doesn't apply the glob and
// is executed only once for the whole graph.
@@ -420,7 +424,7 @@ pub async fn traced_module_data_for_graph(
.await?
.context("NFT module has no content")?
.content()
.hash(HashAlgorithm::Xxh3Hash128Hex)
.hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
.await?,
),
))
+14 -3
View File
@@ -119,6 +119,7 @@ impl Asset for NftJsonAsset {
let output_root_ref = this.project.output_fs().root().await?;
let project_root_ref = this.project.project_fs().root().await?;
let next_config = this.project.next_config();
let hash_salt = next_config.output_hash_salt();
let client_root = this.project.client_fs().root();
let client_root = client_root.owned().await?;
@@ -173,7 +174,11 @@ impl Asset for NftJsonAsset {
let (referenced_chunk_path, hash) = match referenced {
AssetOrModule::Asset(v) => (
Either::Left(v.path().await?),
Either::Left(v.content().hash(HashAlgorithm::Xxh3Hash128Hex).await?),
Either::Left(
v.content()
.hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
.await?,
),
),
AssetOrModule::Module(v) => {
let ident = module_data
@@ -235,7 +240,10 @@ impl Asset for NftJsonAsset {
Ok((
relative_path,
Either::Left(
file_path.read().hash(HashAlgorithm::Xxh3Hash128Hex).await?,
file_path
.read()
.hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
.await?,
),
))
})
@@ -266,7 +274,10 @@ impl Asset for NftJsonAsset {
// non-adapter consumers (which includes output:standalone) don't experience a breaking
// change, but instead we just add it as a separate field that only build-complete
// reads.
let entry_hash = chunk.content().hash(HashAlgorithm::Xxh3Hash128Hex).await?;
let entry_hash = chunk
.content()
.hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
.await?;
let json = json!({
"version": 1,
"files": files,
+1 -1
View File
@@ -46,7 +46,7 @@ async fn asset_path(
} else {
asset
.content()
.hash(HashAlgorithm::Xxh3Hash128Hex)
.hash(no_hash_salt(), HashAlgorithm::Xxh3Hash128Hex)
.owned()
.await?
};
+22 -7
View File
@@ -5,7 +5,7 @@ use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoi
use turbo_tasks_fs::{FileContent, FileSystemPath};
use turbo_tasks_hash::{DeterministicHash, HashAlgorithm, Xxh3Hash64Hasher, hash_xxh3_hash64};
use turbopack_core::{
asset::{Asset, AssetContent},
asset::{Asset, AssetContent, no_hash_salt},
module::{Module, Modules},
module_graph::{GraphTraversalAction, ModuleGraph},
output::{
@@ -62,7 +62,7 @@ pub async fn endpoints_outputs(endpoints: Vc<Endpoints>) -> Result<Vc<OutputAsse
}
#[turbo_tasks::function]
pub async fn outputs_hash(outputs: Vc<OutputAssets>) -> Result<Vc<u64>> {
pub async fn outputs_hash(outputs: Vc<OutputAssets>, hash_salt: Vc<RcStr>) -> Result<Vc<u64>> {
let output_assets = expand_output_assets(
outputs
.await?
@@ -73,7 +73,11 @@ pub async fn outputs_hash(outputs: Vc<OutputAssets>) -> Result<Vc<u64>> {
.await?;
let outputs_hashes = output_assets
.iter()
.map(|asset| asset.content().hash(HashAlgorithm::Xxh3Hash128Hex))
.map(|asset| {
asset
.content()
.hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
})
.try_join()
.await?;
@@ -121,7 +125,11 @@ pub async fn endpoints_entry_modules(
}
#[turbo_tasks::function]
pub async fn sources_hash(module_graph: Vc<ModuleGraph>, modules: Vc<Modules>) -> Result<Vc<u64>> {
pub async fn sources_hash(
module_graph: Vc<ModuleGraph>,
modules: Vc<Modules>,
hash_salt: Vc<RcStr>,
) -> Result<Vc<u64>> {
let modules = modules.await?;
let mut all_modules = FxIndexSet::default();
@@ -144,7 +152,11 @@ pub async fn sources_hash(module_graph: Vc<ModuleGraph>, modules: Vc<Modules>) -
.try_flat_join()
.await?
.into_iter()
.map(|source| source.content().hash(HashAlgorithm::Xxh3Hash128Hex))
.map(|source| {
source
.content()
.hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
})
.try_join()
.await?;
@@ -181,6 +193,7 @@ impl RoutesHashesManifestAsset {
impl Asset for RoutesHashesManifestAsset {
#[turbo_tasks::function]
async fn content(&self) -> Result<Vc<AssetContent>> {
let hash_salt = no_hash_salt();
let module_graphs = self.project.whole_app_module_graphs().await?;
let base_module_graph = *module_graphs.base;
let full_module_graph = *module_graphs.full;
@@ -195,8 +208,9 @@ impl Asset for RoutesHashesManifestAsset {
sources_hash(
full_module_graph,
endpoint_entry_modules(base_module_graph, *entry.endpoint),
hash_salt,
),
outputs_hash(endpoint_outputs(*entry.endpoint)),
outputs_hash(endpoint_outputs(*entry.endpoint), hash_salt),
)
} else {
let endpoints = Vc::cell(primary.iter().map(|entry| entry.endpoint).collect());
@@ -204,8 +218,9 @@ impl Asset for RoutesHashesManifestAsset {
sources_hash(
full_module_graph,
endpoints_entry_modules(base_module_graph, endpoints),
hash_salt,
),
outputs_hash(endpoints_outputs(endpoints)),
outputs_hash(endpoints_outputs(endpoints), hash_salt),
)
};
entrypoint_hashes.insert(key.as_str(), entry);
+24 -7
View File
@@ -231,11 +231,11 @@ impl Asset for ServerActionManifestAsset {
let actions_value = self.actions.await?;
let async_module_info = self.module_graph.async_module_info();
let durable_use_cache_entries = *self
.project
.next_config()
let next_config = self.project.next_config();
let durable_use_cache_entries = *next_config
.enable_durable_use_cache_entries(self.project.next_mode())
.await?;
let hash_salt = next_config.output_hash_salt();
let loader_id = self.chunk_item.id().await?;
let loader_id = match &loader_id {
@@ -279,6 +279,7 @@ impl Asset for ServerActionManifestAsset {
*self.module_graph,
**module,
*self.chunking_context,
hash_salt,
)
.await?,
)
@@ -361,6 +362,7 @@ async fn compute_subtree_content_hash(
module_graph: ResolvedVc<ModuleGraph>,
entry: ResolvedVc<Box<dyn Module>>,
chunking_context: Vc<Box<dyn ChunkingContext>>,
hash_salt: Vc<RcStr>,
) -> Result<Vc<RcStr>> {
let span = tracing::info_span!(
"compute use-cache code hash",
@@ -408,8 +410,14 @@ async fn compute_subtree_content_hash(
.map(async |m| Ok(format!(
" '{}': {}",
m.ident_string().await?,
module_hash(*module_graph, chunking_context, async_module_info, **m)
.await?
module_hash(
*module_graph,
chunking_context,
async_module_info,
**m,
hash_salt
)
.await?
)))
.try_join()
.await?
@@ -419,7 +427,15 @@ async fn compute_subtree_content_hash(
let hashes = modules
.into_iter()
.map(|m| module_hash(*module_graph, chunking_context, async_module_info, *m))
.map(|m| {
module_hash(
*module_graph,
chunking_context,
async_module_info,
*m,
hash_salt,
)
})
.try_join()
.await?;
@@ -448,6 +464,7 @@ async fn module_hash(
chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
async_module_info: ResolvedVc<AsyncModulesInfo>,
m: ResolvedVc<Box<dyn Module>>,
hash_salt: Vc<RcStr>,
) -> Result<Vc<RcStr>> {
let ident = m.ident();
let ident_value = ident.await?;
@@ -484,7 +501,7 @@ async fn module_hash(
.await?
.with_context(|| format!("failed to get source for module {ident_str}"))?
.content()
.hash(HashAlgorithm::Xxh3Hash128Hex)
.hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
.await?;
Ok(Vc::cell(RcStr::from(deterministic_hash(
"",
@@ -1046,7 +1046,8 @@ export async function handleBuildComplete({
existingOutput.assetsHashes,
path.relative(repoRoot, pageFile),
pageFile,
bundler
bundler,
config.experimental.outputHashSalt || ''
)
continue
}
@@ -2170,6 +2171,7 @@ async function getSharedNodeAssets({
const pagesSharedNodeAssetsHashes: Record<string, string> = {}
const appPagesSharedNodeAssets: Record<string, string> = {}
const appPagesSharedNodeAssetsHashes: Record<string, string> = {}
const salt = config.experimental.outputHashSalt || ''
const moduleTypes = ['app-page', 'pages'] as const
@@ -2201,7 +2203,8 @@ async function getSharedNodeAssets({
pagesSharedNodeAssetsHashes,
rootRelativeFilePath,
path.join(repoRoot, rootRelativeFilePath),
bundler
bundler,
salt
)
} else {
await pushAsset(
@@ -2209,7 +2212,8 @@ async function getSharedNodeAssets({
appPagesSharedNodeAssetsHashes,
rootRelativeFilePath,
path.join(repoRoot, rootRelativeFilePath),
bundler
bundler,
salt
)
}
}
@@ -2226,7 +2230,8 @@ async function getSharedNodeAssets({
sharedNodeAssetsHashes,
path.relative(repoRoot, setupNodeStubPath),
require.resolve('next/dist/build/adapter/setup-node-env.external'),
bundler
bundler,
salt
)
// Turbopack handles this automatically and these files are listed in the nft.json files.
@@ -2315,7 +2320,8 @@ async function getSharedNodeAssets({
sharedNodeAssetsHashes,
path.relative(repoRoot, absoluteFilePath),
absoluteFilePath,
bundler
bundler,
salt
)
}
}
@@ -2338,6 +2344,7 @@ async function getSharedNodeAssets({
fileOutputPath,
path.join(distDir, 'server', 'instrumentation.js'),
bundler,
salt,
instrumentationEntryHash
)
}
@@ -2352,7 +2359,8 @@ async function getSharedNodeAssets({
sharedNodeAssetsHashes,
fileOutputPath,
filePath,
bundler
bundler,
salt
)
}
@@ -2372,13 +2380,14 @@ async function pushAsset(
targetFilePath: string,
sourceFilePath: string,
bundler: Bundler,
salt: string,
hashOverride?: string
) {
if (!(targetFilePath in assets)) {
assets[targetFilePath] = sourceFilePath
if (bundler === Bundler.Turbopack) {
assetsHashes[targetFilePath] =
hashOverride ?? (await hashFile(sourceFilePath))
hashOverride ?? (await hashFile(salt, sourceFilePath))
}
}
}
@@ -2411,8 +2420,9 @@ async function loadNFT(
return { entryHash }
}
async function hashFile(filePath: string): Promise<string> {
async function hashFile(salt: string, filePath: string): Promise<string> {
const hash = crypto.createHash('sha256')
hash.update(salt)
try {
// Try symlink first, since readFile just transparently resolves those (or fails if it's a
// directory symlink).
@@ -61,6 +61,56 @@ import { FILES } from './files'
outputs.pagesApi.forEach(validateOutput)
outputs.appRoutes.forEach(validateOutput)
})
it('hashes respect NEXT_HASH_SALT', async () => {
const {
outputs: outputs1,
}: Parameters<NextAdapter['onBuildComplete']>[0] = await next.readJSON(
'build-complete.json'
)
await next.stop()
next.env.NEXT_HASH_SALT = 'something-else'
await next.build()
const {
outputs: outputs2,
}: Parameters<NextAdapter['onBuildComplete']>[0] = await next.readJSON(
'build-complete.json'
)
let functions1 = Object.fromEntries(
[
...outputs1.pages,
...outputs1.pagesApi,
...outputs1.appPages,
...outputs1.appRoutes,
].map((output) => [output.pathname, output.assetsHashes])
)
let functions2 = Object.fromEntries(
[
...outputs2.pages,
...outputs2.pagesApi,
...outputs2.appPages,
...outputs2.appRoutes,
].map((output) => [output.pathname, output.assetsHashes])
)
for (const pathname in functions1) {
const function1 = functions1[pathname]
const function2 = functions2[pathname]
for (const file in function1) {
const hash1 = function1[file]
const hash2 = function2[file]
expect(hash1).toBeString()
if (hash1 === hash2) {
throw new Error(
`Hash for ${pathname} file ${file} did not change with NEXT_HASH_SALT: ${hash1}`
)
}
}
}
})
})
}
)
+6 -3
View File
@@ -2531,9 +2531,12 @@ impl FileContent {
}
#[turbo_tasks::function]
pub fn hash(&self, algorithm: HashAlgorithm) -> Vc<RcStr> {
// no_hash_salt
Vc::cell(RcStr::from(deterministic_hash("", self, algorithm)))
pub async fn hash(&self, salt: Vc<RcStr>, algorithm: HashAlgorithm) -> Result<Vc<RcStr>> {
Ok(Vc::cell(RcStr::from(deterministic_hash(
&salt.await?,
self,
algorithm,
))))
}
/// Converts this [`FileContent`] into a [`PersistedFileContent`] by cloning.
+5 -5
View File
@@ -131,14 +131,14 @@ impl AssetContent {
}
#[turbo_tasks::function]
pub fn hash(&self, algorithm: HashAlgorithm) -> Vc<RcStr> {
match self {
AssetContent::File(content) => content.hash(algorithm),
pub async fn hash(&self, salt: Vc<RcStr>, algorithm: HashAlgorithm) -> Result<Vc<RcStr>> {
Ok(match self {
AssetContent::File(content) => content.hash(salt, algorithm),
AssetContent::Redirect { target, link_type } => Vc::cell(RcStr::from(
// no_hash_salt
deterministic_hash("", (target, link_type), algorithm),
deterministic_hash(&salt.await?, (target, link_type), algorithm),
)),
}
})
}
/// Compared to [AssetContent::hash], this hashes only the bytes of the file content and