1use anyhow::Result;
2use serde::Serialize;
3use turbo_rcstr::RcStr;
4use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
5use turbo_tasks_fs::{FileContent, FileSystemPath};
6use turbo_tasks_hash::{DeterministicHash, HashAlgorithm, Xxh3Hash64Hasher, hash_xxh3_hash64};
7use turbopack_core::{
8 asset::{Asset, AssetContent, no_hash_salt},
9 module::{Module, Modules},
10 module_graph::{GraphTraversalAction, ModuleGraph},
11 output::{
12 ExpandOutputAssetsInput, OutputAsset, OutputAssets, OutputAssetsReference,
13 expand_output_assets,
14 },
15};
16
17use crate::{
18 project::Project,
19 route::{Endpoint, EndpointGroup, Endpoints},
20};
21
22#[turbo_tasks::value(shared)]
23pub struct EndpointHashes {
24 pub sources_hash: u64,
25 pub outputs_hash: u64,
26}
27
28impl EndpointHashes {
29 pub fn merge<'l>(iterator: impl Iterator<Item = (Option<RcStr>, &'l EndpointHashes)>) -> Self {
30 let mut sources_hasher = Xxh3Hash64Hasher::new();
31 let mut outputs_hasher = Xxh3Hash64Hasher::new();
32
33 for (key, hashes) in iterator {
34 key.deterministic_hash(&mut sources_hasher);
35 key.deterministic_hash(&mut outputs_hasher);
36 hashes.sources_hash.deterministic_hash(&mut sources_hasher);
37 hashes.outputs_hash.deterministic_hash(&mut outputs_hasher);
38 }
39
40 Self {
41 sources_hash: sources_hasher.finish(),
42 outputs_hash: outputs_hasher.finish(),
43 }
44 }
45}
46
47#[turbo_tasks::function]
48pub async fn endpoint_outputs(endpoint: Vc<Box<dyn Endpoint>>) -> Result<Vc<OutputAssets>> {
49 Ok(*endpoint.output().await?.output_assets)
50}
51
52#[turbo_tasks::function]
53pub async fn endpoints_outputs(endpoints: Vc<Endpoints>) -> Result<Vc<OutputAssets>> {
54 let endpoints = endpoints.await?;
55 let all_outputs = endpoints
56 .iter()
57 .map(async |endpoint| Ok(endpoint.output().await?.output_assets.await?))
58 .try_join()
59 .await?;
60 let set = all_outputs.into_iter().flatten().collect::<FxIndexSet<_>>();
61 Ok(Vc::cell(set.into_iter().collect()))
62}
63
64#[turbo_tasks::function]
65pub async fn outputs_hash(outputs: Vc<OutputAssets>, hash_salt: Vc<RcStr>) -> Result<Vc<u64>> {
66 let output_assets = expand_output_assets(
67 outputs
68 .await?
69 .into_iter()
70 .map(ExpandOutputAssetsInput::Asset),
71 true,
72 )
73 .await?;
74 let outputs_hashes = output_assets
75 .iter()
76 .map(|asset| {
77 asset
78 .content()
79 .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
80 })
81 .try_join()
82 .await?;
83
84 Ok(Vc::cell(hash_xxh3_hash64(outputs_hashes)))
85}
86
87#[turbo_tasks::function]
88pub async fn endpoint_entry_modules(
89 base_module_graph: Vc<ModuleGraph>,
90 endpoint: Vc<Box<dyn Endpoint>>,
91) -> Result<Vc<Modules>> {
92 let entries = endpoint.entries().await?;
93 let additional_entries = endpoint.additional_entries(base_module_graph).await?;
94 let modules = entries
95 .chunk_group_modules()
96 .chain(additional_entries.chunk_group_modules())
97 .collect::<FxIndexSet<_>>();
98 Ok(Vc::cell(modules.into_iter().collect()))
99}
100
101#[turbo_tasks::function]
102pub async fn endpoints_entry_modules(
103 base_module_graph: Vc<ModuleGraph>,
104 endpoints: Vc<Endpoints>,
105) -> Result<Vc<Modules>> {
106 let endpoints = endpoints.await?;
107 let entries_and_additional_entries = endpoints
108 .iter()
109 .map(async |endpoint| {
110 let entries = endpoint.entries();
111 let additional_entries = endpoint.additional_entries(base_module_graph);
112 Ok((entries.await?, additional_entries.await?))
113 })
114 .try_join()
115 .await?;
116 let modules = entries_and_additional_entries
117 .iter()
118 .flat_map(|(entries, additional_entries)| {
119 entries
120 .chunk_group_modules()
121 .chain(additional_entries.chunk_group_modules())
122 })
123 .collect::<FxIndexSet<_>>();
124 Ok(Vc::cell(modules.into_iter().collect()))
125}
126
127#[turbo_tasks::function]
128pub async fn sources_hash(
129 module_graph: Vc<ModuleGraph>,
130 modules: Vc<Modules>,
131 hash_salt: Vc<RcStr>,
132) -> Result<Vc<u64>> {
133 let modules = modules.await?;
134
135 let mut all_modules = FxIndexSet::default();
136
137 let module_graph = module_graph.await?;
138
139 module_graph.traverse_nodes_dfs(
140 modules,
141 &mut all_modules,
142 |module, all_modules| {
143 all_modules.insert(*module);
144 Ok(GraphTraversalAction::Continue)
145 },
146 |_, _| Ok(()),
147 )?;
148
149 let sources = all_modules
150 .iter()
151 .map(|module| module.source())
152 .try_flat_join()
153 .await?
154 .into_iter()
155 .map(|source| {
156 source
157 .content()
158 .hash(hash_salt, HashAlgorithm::Xxh3Hash128Hex)
159 })
160 .try_join()
161 .await?;
162
163 Ok(Vc::cell(hash_xxh3_hash64(sources)))
164}
165
166#[derive(Serialize)]
167struct RoutesHashesManifest<'l> {
168 pub routes: FxIndexMap<&'l str, EndpointHashStrings>,
169}
170
171#[derive(Serialize)]
172#[serde(rename_all = "camelCase")]
173pub struct EndpointHashStrings {
174 pub sources_hash: String,
175 pub outputs_hash: String,
176}
177
178#[turbo_tasks::value]
179pub struct RoutesHashesManifestAsset {
180 path: FileSystemPath,
181 project: ResolvedVc<Project>,
182}
183
184#[turbo_tasks::value_impl]
185impl RoutesHashesManifestAsset {
186 #[turbo_tasks::function]
187 pub fn new(path: FileSystemPath, project: ResolvedVc<Project>) -> Vc<Self> {
188 RoutesHashesManifestAsset { path, project }.cell()
189 }
190}
191
192#[turbo_tasks::value_impl]
193impl Asset for RoutesHashesManifestAsset {
194 #[turbo_tasks::function]
195 async fn content(&self) -> Result<Vc<AssetContent>> {
196 let hash_salt = no_hash_salt();
197 let module_graphs = self.project.whole_app_module_graphs().await?;
198 let base_module_graph = *module_graphs.base;
199 let full_module_graph = *module_graphs.full;
200
201 let mut entrypoint_hashes = FxIndexMap::default();
202
203 let entrypoint_groups = self.project.get_all_endpoint_groups(false).await?;
204
205 for (key, EndpointGroup { primary, .. }) in &entrypoint_groups {
206 let entry = if let &[entry] = &primary.as_slice() {
207 (
208 sources_hash(
209 full_module_graph,
210 endpoint_entry_modules(base_module_graph, *entry.endpoint),
211 hash_salt,
212 ),
213 outputs_hash(endpoint_outputs(*entry.endpoint), hash_salt),
214 )
215 } else {
216 let endpoints = Vc::cell(primary.iter().map(|entry| entry.endpoint).collect());
217 (
218 sources_hash(
219 full_module_graph,
220 endpoints_entry_modules(base_module_graph, endpoints),
221 hash_salt,
222 ),
223 outputs_hash(endpoints_outputs(endpoints), hash_salt),
224 )
225 };
226 entrypoint_hashes.insert(key.as_str(), entry);
227 }
228
229 let entrypoint_hashes_values = entrypoint_hashes
230 .values()
231 .map(async |(sources_hash, outputs_hash)| {
232 Ok((sources_hash.await?, outputs_hash.await?))
233 })
234 .try_join()
235 .await?;
236
237 let manifest = serde_json::to_string_pretty(&RoutesHashesManifest {
238 routes: entrypoint_hashes
239 .into_keys()
240 .zip(entrypoint_hashes_values)
241 .map(|(k, (sources_hash, outputs_hash))| {
242 (
243 k,
244 EndpointHashStrings {
245 sources_hash: format!("{:016x}", *sources_hash),
246 outputs_hash: format!("{:016x}", *outputs_hash),
247 },
248 )
249 })
250 .collect(),
251 })?;
252 Ok(AssetContent::File(FileContent::Content(manifest.into()).resolved_cell()).cell())
253 }
254}
255
256#[turbo_tasks::value_impl]
257impl OutputAssetsReference for RoutesHashesManifestAsset {}
258
259#[turbo_tasks::value_impl]
260impl OutputAsset for RoutesHashesManifestAsset {
261 #[turbo_tasks::function]
262 fn path(&self) -> Vc<FileSystemPath> {
263 self.path.clone().cell()
264 }
265}
266
267#[turbo_tasks::function]
268pub async fn routes_hashes_manifest_asset_if_enabled(
269 project: ResolvedVc<Project>,
270) -> Result<Vc<OutputAssets>> {
271 let should_write = *project.should_write_routes_hashes_manifest().await?;
272 let assets = if should_write {
273 let path = project
274 .node_root()
275 .await?
276 .join("diagnostics/routes-hashes-manifest.json")?;
277 let asset = RoutesHashesManifestAsset::new(path, *project)
278 .to_resolved()
279 .await?;
280 vec![ResolvedVc::upcast(asset)]
281 } else {
282 vec![]
283 };
284 Ok(Vc::cell(assets))
285}