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, Xxh3Hash64Hasher};
7use turbopack_core::{
8 asset::{Asset, AssetContent},
9 module::Module,
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},
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_hashes(
49 base_module_graph: Vc<ModuleGraph>,
50 module_graph: Vc<ModuleGraph>,
51 endpoint: Vc<Box<dyn Endpoint>>,
52) -> Result<Vc<EndpointHashes>> {
53 let entries = endpoint.entries();
54 let additional_entries = endpoint.additional_entries(base_module_graph);
55 let modules = entries
56 .await?
57 .into_iter()
58 .chain(additional_entries.await?.into_iter())
59 .flat_map(|e| e.entries())
60 .collect::<FxIndexSet<_>>();
61 let outputs = endpoint.output();
62
63 let mut all_modules = FxIndexSet::default();
64
65 let module_graph = module_graph.read_graphs().await?;
66
67 module_graph.traverse_nodes_from_entries_dfs(
68 modules,
69 &mut all_modules,
70 |module, all_modules| {
71 all_modules.insert(*module);
72 Ok(GraphTraversalAction::Continue)
73 },
74 |_, _| Ok(()),
75 )?;
76
77 let sources = all_modules
78 .iter()
79 .map(|module| module.source())
80 .try_flat_join()
81 .await?
82 .into_iter()
83 .map(|source| source.content().hash())
84 .try_join()
85 .await?;
86
87 let output_assets = expand_output_assets(
88 outputs
89 .await?
90 .output_assets
91 .await?
92 .into_iter()
93 .map(|asset| ExpandOutputAssetsInput::Asset(*asset)),
94 true,
95 )
96 .await?;
97 let outputs_hashes = output_assets
98 .iter()
99 .map(|asset| asset.content().hash())
100 .try_join()
101 .await?;
102
103 let sources_hash = {
104 let mut hasher = Xxh3Hash64Hasher::new();
105 for source in sources.iter() {
106 source.deterministic_hash(&mut hasher);
107 }
108 hasher.finish()
109 };
110 let outputs_hash = {
111 let mut hasher = Xxh3Hash64Hasher::new();
112 for hash in outputs_hashes.iter() {
113 hash.deterministic_hash(&mut hasher);
114 }
115 hasher.finish()
116 };
117
118 Ok(EndpointHashes {
119 sources_hash,
120 outputs_hash,
121 }
122 .cell())
123}
124
125#[derive(Serialize)]
126struct RoutesHashesManifest<'l> {
127 pub routes: FxIndexMap<&'l str, EndpointHashStrings>,
128}
129
130#[derive(Serialize)]
131#[serde(rename_all = "camelCase")]
132pub struct EndpointHashStrings {
133 pub sources_hash: String,
134 pub outputs_hash: String,
135}
136
137#[turbo_tasks::value]
138pub struct RoutesHashesManifestAsset {
139 path: FileSystemPath,
140 project: ResolvedVc<Project>,
141}
142
143#[turbo_tasks::value_impl]
144impl RoutesHashesManifestAsset {
145 #[turbo_tasks::function]
146 pub fn new(path: FileSystemPath, project: ResolvedVc<Project>) -> Vc<Self> {
147 RoutesHashesManifestAsset { path, project }.cell()
148 }
149}
150
151#[turbo_tasks::value_impl]
152impl Asset for RoutesHashesManifestAsset {
153 #[turbo_tasks::function]
154 async fn content(&self) -> Result<Vc<AssetContent>> {
155 let module_graphs = self.project.whole_app_module_graphs().await?;
156 let base_module_graph = *module_graphs.base;
157 let full_module_graph = *module_graphs.full;
158
159 let mut entrypoint_hashes = FxIndexMap::default();
160
161 let entrypoint_groups = self.project.get_all_endpoint_groups(false).await?;
162
163 for (key, EndpointGroup { primary, .. }) in entrypoint_groups {
164 if let &[entry] = &primary.as_slice() {
165 entrypoint_hashes.insert(
166 key.as_str(),
167 endpoint_hashes(base_module_graph, full_module_graph, *entry.endpoint),
168 );
169 } else {
170 let hashes = primary
171 .iter()
172 .map(|entry| {
173 endpoint_hashes(base_module_graph, full_module_graph, *entry.endpoint)
174 })
175 .try_join()
176 .await?;
177 let hashes = EndpointHashes::merge(
178 primary
179 .iter()
180 .map(|page| page.sub_name.clone())
181 .zip(hashes.iter())
182 .map(|(k, v)| (k, &**v)),
183 )
184 .cell();
185 entrypoint_hashes.insert(key.as_str(), hashes);
186 }
187 }
188
189 let entrypoint_hashes_values = entrypoint_hashes.values().copied().try_join().await?;
190
191 let manifest = serde_json::to_string_pretty(&RoutesHashesManifest {
192 routes: entrypoint_hashes
193 .into_keys()
194 .zip(entrypoint_hashes_values.into_iter())
195 .map(|(k, v)| {
196 (
197 k,
198 EndpointHashStrings {
199 sources_hash: format!("{:016x}", v.sources_hash),
200 outputs_hash: format!("{:016x}", v.outputs_hash),
201 },
202 )
203 })
204 .collect(),
205 })?;
206 Ok(AssetContent::File(FileContent::Content(manifest.into()).resolved_cell()).cell())
207 }
208}
209
210#[turbo_tasks::value_impl]
211impl OutputAssetsReference for RoutesHashesManifestAsset {}
212
213#[turbo_tasks::value_impl]
214impl OutputAsset for RoutesHashesManifestAsset {
215 #[turbo_tasks::function]
216 fn path(&self) -> Vc<FileSystemPath> {
217 self.path.clone().cell()
218 }
219}
220
221#[turbo_tasks::function]
222pub async fn routes_hashes_manifest_asset_if_enabled(
223 project: ResolvedVc<Project>,
224) -> Result<Vc<OutputAssets>> {
225 let should_write = *project.should_write_routes_hashes_manifest().await?;
226 let assets = if should_write {
227 let path = project
228 .node_root()
229 .await?
230 .join("diagnostics/routes-hashes-manifest.json")?;
231 let asset = RoutesHashesManifestAsset::new(path, *project)
232 .to_resolved()
233 .await?;
234 vec![ResolvedVc::upcast(asset)]
235 } else {
236 vec![]
237 };
238 Ok(Vc::cell(assets))
239}