Skip to main content

next_api/
project_asset_hashes_manifest.rs

1use 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.into_iter().flatten().collect::<FxIndexSet<_>>();
72    Ok(Vc::cell(set.into_iter().collect()))
73}
74
75#[turbo_tasks::value(transparent)]
76pub struct OutputAssetsWithPaths(Vec<(ResolvedVc<Box<dyn OutputAsset>>, RcStr)>);
77
78#[turbo_tasks::function]
79pub async fn expand_outputs(
80    project: Vc<Project>,
81    root: FileSystemPath,
82) -> Result<Vc<OutputAssetsWithPaths>> {
83    let entrypoint_groups = project.get_all_endpoint_groups(false).await?;
84
85    let output_assets = entrypoint_groups
86        .iter()
87        .map(|(_, EndpointGroup { primary, .. })| {
88            if let &[entry] = &primary.as_slice() {
89                endpoint_outputs(*entry.endpoint)
90            } else {
91                let endpoints = Vc::cell(primary.iter().map(|entry| entry.endpoint).collect());
92                endpoints_outputs(endpoints)
93            }
94        })
95        .collect::<Vec<_>>();
96
97    let output_assets = expand_output_assets(
98        output_assets
99            .iter()
100            .try_join()
101            .await?
102            .into_iter()
103            .flatten()
104            .map(ExpandOutputAssetsInput::Asset),
105        true,
106    )
107    .await?;
108
109    let mut output_assets = output_assets
110        .into_iter()
111        .map(async |asset| {
112            if let Some(path) = root.get_path_to(&*asset.path().await?) {
113                Ok(Some((asset, RcStr::from(path))))
114            } else {
115                Ok(None)
116            }
117        })
118        .try_flat_join()
119        .await?;
120
121    // Shared JS assets aren't duplicated here, but we have some duplicate OutputAssets with the
122    // same path, e.g. a static image which exists twice, once with the server and then also with
123    // the client chunking context.
124    output_assets.sort_unstable_by(|(_, a), (_, b)| a.cmp(b));
125    output_assets.dedup_by(|(_, a), (_, b)| a == b);
126
127    Ok(Vc::cell(output_assets))
128}
129
130#[turbo_tasks::value_impl]
131impl Asset for AssetHashesManifestAsset {
132    #[turbo_tasks::function]
133    async fn content(&self) -> Result<Vc<AssetContent>> {
134        let output_assets = expand_outputs(*self.project, self.asset_root.clone()).await?;
135
136        let asset_paths = output_assets
137            .iter()
138            .map(async |(asset, path)| {
139                Ok((
140                    path,
141                    asset
142                        .content_hash(
143                            self.project.next_config().output_hash_salt(),
144                            HashAlgorithm::Xxh3Hash128Base38,
145                        )
146                        .await?,
147                ))
148            })
149            .try_join()
150            .await?;
151
152        struct Manifest<'a> {
153            asset_paths: &'a Vec<(&'a RcStr, ReadRef<Option<RcStr>>)>,
154        }
155
156        impl serde::Serialize for Manifest<'_> {
157            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
158                let mut map = serializer.serialize_map(Some(self.asset_paths.len()))?;
159                for (path, content_hash) in self.asset_paths {
160                    map.serialize_entry(
161                        path,
162                        if let Some(content_hash) = content_hash.as_ref() {
163                            content_hash
164                        } else {
165                            return Err(S::Error::custom("asset content hash failed"));
166                        },
167                    )?;
168                }
169                map.end()
170            }
171        }
172
173        let json = serde_json::to_string(&Manifest {
174            asset_paths: &asset_paths,
175        })?;
176
177        Ok(AssetContent::file(
178            FileContent::Content(File::from(json)).cell(),
179        ))
180    }
181}
182
183#[turbo_tasks::function]
184pub async fn immutable_hashes_manifest_asset_if_enabled(
185    project: ResolvedVc<Project>,
186) -> Result<Vc<OutputAssets>> {
187    if *project.next_config().enable_immutable_assets().await? {
188        let path = project
189            .node_root()
190            .await?
191            .join("immutable-static-hashes.json")?;
192
193        let asset = AssetHashesManifestAsset::new(
194            path,
195            *project,
196            project.client_relative_path().owned().await?,
197        )
198        .to_resolved()
199        .await?;
200        Ok(Vc::cell(vec![ResolvedVc::upcast(asset)]))
201    } else {
202        Ok(OutputAssets::empty())
203    }
204}