mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
Respect NEXT_HASH_SALT for server side assetsHashes (#95738)
Also respect the hash salt for `assetsHashes`
This commit is contained in:
committed by
GitHub
parent
ca414736ba
commit
b3480cfdd5
@@ -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?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?,
|
||||
),
|
||||
))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -46,7 +46,7 @@ async fn asset_path(
|
||||
} else {
|
||||
asset
|
||||
.content()
|
||||
.hash(HashAlgorithm::Xxh3Hash128Hex)
|
||||
.hash(no_hash_salt(), HashAlgorithm::Xxh3Hash128Hex)
|
||||
.owned()
|
||||
.await?
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user