Skip to main content

next_api/
routes_hashes_manifest.rs

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