next_api/
project_asset_hashes_manifest.rs1use anyhow::Result;
2use serde::{
3 Serializer,
4 ser::{Error, SerializeMap},
5};
6use turbo_rcstr::RcStr;
7use turbo_tasks::{FxIndexSet, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
8use turbo_tasks_fs::{File, FileContent, FileSystemPath};
9use turbo_tasks_hash::HashAlgorithm;
10use turbopack_core::{
11 asset::{Asset, AssetContent},
12 output::{
13 ExpandOutputAssetsInput, OutputAsset, OutputAssets, OutputAssetsReference,
14 expand_output_assets,
15 },
16};
17
18use crate::{
19 project::Project,
20 route::{Endpoint, EndpointGroup, Endpoints},
21};
22
23#[turbo_tasks::value]
24struct AssetHashesManifestAsset {
25 output_path: FileSystemPath,
26 project: ResolvedVc<Project>,
27 asset_root: FileSystemPath,
28}
29
30#[turbo_tasks::value_impl]
31impl AssetHashesManifestAsset {
32 #[turbo_tasks::function]
33 pub fn new(
34 output_path: FileSystemPath,
35 project: ResolvedVc<Project>,
36 asset_root: FileSystemPath,
37 ) -> Vc<Self> {
38 Self {
39 output_path,
40 project,
41 asset_root,
42 }
43 .cell()
44 }
45}
46
47#[turbo_tasks::value_impl]
48impl OutputAssetsReference for AssetHashesManifestAsset {}
49
50#[turbo_tasks::value_impl]
51impl OutputAsset for AssetHashesManifestAsset {
52 #[turbo_tasks::function]
53 async fn path(&self) -> Vc<FileSystemPath> {
54 self.output_path.clone().cell()
55 }
56}
57
58#[turbo_tasks::function]
59pub async fn endpoint_outputs(endpoint: Vc<Box<dyn Endpoint>>) -> Result<Vc<OutputAssets>> {
60 Ok(*endpoint.output().await?.output_assets)
61}
62
63#[turbo_tasks::function]
64pub async fn endpoints_outputs(endpoints: Vc<Endpoints>) -> Result<Vc<OutputAssets>> {
65 let endpoints = endpoints.await?;
66 let all_outputs = endpoints
67 .iter()
68 .map(async |endpoint| endpoint.output().await?.output_assets.await)
69 .try_join()
70 .await?;
71 let set = all_outputs
72 .into_iter()
73 .flatten()
74 .copied()
75 .collect::<FxIndexSet<_>>();
76 Ok(Vc::cell(set.into_iter().collect()))
77}
78
79#[turbo_tasks::value(transparent)]
80pub struct OutputAssetsWithPaths(Vec<(ResolvedVc<Box<dyn OutputAsset>>, RcStr)>);
81
82#[turbo_tasks::function]
83pub async fn expand_outputs(
84 project: Vc<Project>,
85 root: FileSystemPath,
86) -> Result<Vc<OutputAssetsWithPaths>> {
87 let entrypoint_groups = project.get_all_endpoint_groups(false).await?;
88
89 let output_assets = entrypoint_groups
90 .iter()
91 .map(|(_, EndpointGroup { primary, .. })| {
92 if let &[entry] = &primary.as_slice() {
93 endpoint_outputs(*entry.endpoint)
94 } else {
95 let endpoints = Vc::cell(primary.iter().map(|entry| entry.endpoint).collect());
96 endpoints_outputs(endpoints)
97 }
98 })
99 .collect::<Vec<_>>();
100
101 let output_assets = expand_output_assets(
102 output_assets
103 .iter()
104 .try_join()
105 .await?
106 .into_iter()
107 .flatten()
108 .map(|asset| ExpandOutputAssetsInput::Asset(*asset)),
109 true,
110 )
111 .await?;
112
113 let mut output_assets = output_assets
114 .into_iter()
115 .map(async |asset| {
116 if let Some(path) = root.get_path_to(&*asset.path().await?) {
117 Ok(Some((asset, RcStr::from(path))))
118 } else {
119 Ok(None)
120 }
121 })
122 .try_flat_join()
123 .await?;
124
125 output_assets.sort_unstable_by(|(_, a), (_, b)| a.cmp(b));
129 output_assets.dedup_by(|(_, a), (_, b)| a == b);
130
131 Ok(Vc::cell(output_assets))
132}
133
134#[turbo_tasks::value_impl]
135impl Asset for AssetHashesManifestAsset {
136 #[turbo_tasks::function(root)]
137 async fn content(&self) -> Result<Vc<AssetContent>> {
138 let output_assets = expand_outputs(*self.project, self.asset_root.clone()).await?;
139
140 let asset_paths = output_assets
141 .into_iter()
142 .map(async |(asset, path)| {
143 Ok((
144 path,
145 asset.content_hash(HashAlgorithm::Xxh3Hash128Hex).await?,
146 ))
147 })
148 .try_join()
149 .await?;
150
151 struct Manifest<'a> {
152 asset_paths: &'a Vec<(&'a RcStr, ReadRef<Option<RcStr>>)>,
153 }
154
155 impl serde::Serialize for Manifest<'_> {
156 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
157 let mut map = serializer.serialize_map(Some(self.asset_paths.len()))?;
158 for (path, content_hash) in self.asset_paths {
159 map.serialize_entry(
160 path,
161 if let Some(content_hash) = content_hash.as_ref() {
162 content_hash
163 } else {
164 return Err(S::Error::custom("asset content hash failed"));
165 },
166 )?;
167 }
168 map.end()
169 }
170 }
171
172 let json = serde_json::to_string(&Manifest {
173 asset_paths: &asset_paths,
174 })?;
175
176 Ok(AssetContent::file(
177 FileContent::Content(File::from(json)).cell(),
178 ))
179 }
180}
181
182#[turbo_tasks::function]
183pub async fn immutable_hashes_manifest_asset_if_enabled(
184 project: ResolvedVc<Project>,
185) -> Result<Vc<OutputAssets>> {
186 if *project.next_config().enable_immutable_assets().await? {
187 let path = project
188 .node_root()
189 .await?
190 .join("immutable-static-hashes.json")?;
191
192 let asset = AssetHashesManifestAsset::new(
193 path,
194 *project,
195 project.client_relative_path().owned().await?,
196 )
197 .to_resolved()
198 .await?;
199 Ok(Vc::cell(vec![ResolvedVc::upcast(asset)]))
200 } else {
201 Ok(OutputAssets::empty())
202 }
203}