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,
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}