next_api/
routes_hashes_manifest.rs1use 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},
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>) -> 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| asset.content().hash(HashAlgorithm::Xxh3Hash128Hex))
77 .try_join()
78 .await?;
79
80 Ok(Vc::cell(hash_xxh3_hash64(outputs_hashes)))
81}
82
83#[turbo_tasks::function]
84pub async fn endpoint_entry_modules(
85 base_module_graph: Vc<ModuleGraph>,
86 endpoint: Vc<Box<dyn Endpoint>>,
87) -> Result<Vc<Modules>> {
88 let entries = endpoint.entries().await?;
89 let additional_entries = endpoint.additional_entries(base_module_graph).await?;
90 let modules = entries
91 .iter()
92 .chain(additional_entries.iter())
93 .flat_map(|e| e.entries())
94 .collect::<FxIndexSet<_>>();
95 Ok(Vc::cell(modules.into_iter().collect()))
96}
97
98#[turbo_tasks::function]
99pub async fn endpoints_entry_modules(
100 base_module_graph: Vc<ModuleGraph>,
101 endpoints: Vc<Endpoints>,
102) -> Result<Vc<Modules>> {
103 let endpoints = endpoints.await?;
104 let entries_and_additional_entries = endpoints
105 .iter()
106 .map(async |endpoint| {
107 let entries = endpoint.entries();
108 let additional_entries = endpoint.additional_entries(base_module_graph);
109 Ok((entries.await?, additional_entries.await?))
110 })
111 .try_join()
112 .await?;
113 let modules = entries_and_additional_entries
114 .iter()
115 .flat_map(|(entries, additional_entries)| {
116 entries
117 .iter()
118 .chain(additional_entries.iter())
119 .flat_map(|e| e.entries())
120 })
121 .collect::<FxIndexSet<_>>();
122 Ok(Vc::cell(modules.into_iter().collect()))
123}
124
125#[turbo_tasks::function]
126pub async fn sources_hash(module_graph: Vc<ModuleGraph>, modules: Vc<Modules>) -> Result<Vc<u64>> {
127 let modules = modules.await?;
128
129 let mut all_modules = FxIndexSet::default();
130
131 let module_graph = module_graph.await?;
132
133 module_graph.traverse_nodes_dfs(
134 modules,
135 &mut all_modules,
136 |module, all_modules| {
137 all_modules.insert(*module);
138 Ok(GraphTraversalAction::Continue)
139 },
140 |_, _| Ok(()),
141 )?;
142
143 let sources = all_modules
144 .iter()
145 .map(|module| module.source())
146 .try_flat_join()
147 .await?
148 .into_iter()
149 .map(|source| source.content().hash(HashAlgorithm::Xxh3Hash128Hex))
150 .try_join()
151 .await?;
152
153 Ok(Vc::cell(hash_xxh3_hash64(sources)))
154}
155
156#[derive(Serialize)]
157struct RoutesHashesManifest<'l> {
158 pub routes: FxIndexMap<&'l str, EndpointHashStrings>,
159}
160
161#[derive(Serialize)]
162#[serde(rename_all = "camelCase")]
163pub struct EndpointHashStrings {
164 pub sources_hash: String,
165 pub outputs_hash: String,
166}
167
168#[turbo_tasks::value]
169pub struct RoutesHashesManifestAsset {
170 path: FileSystemPath,
171 project: ResolvedVc<Project>,
172}
173
174#[turbo_tasks::value_impl]
175impl RoutesHashesManifestAsset {
176 #[turbo_tasks::function]
177 pub fn new(path: FileSystemPath, project: ResolvedVc<Project>) -> Vc<Self> {
178 RoutesHashesManifestAsset { path, project }.cell()
179 }
180}
181
182#[turbo_tasks::value_impl]
183impl Asset for RoutesHashesManifestAsset {
184 #[turbo_tasks::function]
185 async fn content(&self) -> Result<Vc<AssetContent>> {
186 let module_graphs = self.project.whole_app_module_graphs().await?;
187 let base_module_graph = *module_graphs.base;
188 let full_module_graph = *module_graphs.full;
189
190 let mut entrypoint_hashes = FxIndexMap::default();
191
192 let entrypoint_groups = self.project.get_all_endpoint_groups(false).await?;
193
194 for (key, EndpointGroup { primary, .. }) in &entrypoint_groups {
195 let entry = if let &[entry] = &primary.as_slice() {
196 (
197 sources_hash(
198 full_module_graph,
199 endpoint_entry_modules(base_module_graph, *entry.endpoint),
200 ),
201 outputs_hash(endpoint_outputs(*entry.endpoint)),
202 )
203 } else {
204 let endpoints = Vc::cell(primary.iter().map(|entry| entry.endpoint).collect());
205 (
206 sources_hash(
207 full_module_graph,
208 endpoints_entry_modules(base_module_graph, endpoints),
209 ),
210 outputs_hash(endpoints_outputs(endpoints)),
211 )
212 };
213 entrypoint_hashes.insert(key.as_str(), entry);
214 }
215
216 let entrypoint_hashes_values = entrypoint_hashes
217 .values()
218 .map(async |(sources_hash, outputs_hash)| {
219 Ok((sources_hash.await?, outputs_hash.await?))
220 })
221 .try_join()
222 .await?;
223
224 let manifest = serde_json::to_string_pretty(&RoutesHashesManifest {
225 routes: entrypoint_hashes
226 .into_keys()
227 .zip(entrypoint_hashes_values)
228 .map(|(k, (sources_hash, outputs_hash))| {
229 (
230 k,
231 EndpointHashStrings {
232 sources_hash: format!("{:016x}", *sources_hash),
233 outputs_hash: format!("{:016x}", *outputs_hash),
234 },
235 )
236 })
237 .collect(),
238 })?;
239 Ok(AssetContent::File(FileContent::Content(manifest.into()).resolved_cell()).cell())
240 }
241}
242
243#[turbo_tasks::value_impl]
244impl OutputAssetsReference for RoutesHashesManifestAsset {}
245
246#[turbo_tasks::value_impl]
247impl OutputAsset for RoutesHashesManifestAsset {
248 #[turbo_tasks::function]
249 fn path(&self) -> Vc<FileSystemPath> {
250 self.path.clone().cell()
251 }
252}
253
254#[turbo_tasks::function]
255pub async fn routes_hashes_manifest_asset_if_enabled(
256 project: ResolvedVc<Project>,
257) -> Result<Vc<OutputAssets>> {
258 let should_write = *project.should_write_routes_hashes_manifest().await?;
259 let assets = if should_write {
260 let path = project
261 .node_root()
262 .await?
263 .join("diagnostics/routes-hashes-manifest.json")?;
264 let asset = RoutesHashesManifestAsset::new(path, *project)
265 .to_resolved()
266 .await?;
267 vec![ResolvedVc::upcast(asset)]
268 } else {
269 vec![]
270 };
271 Ok(Vc::cell(assets))
272}