next.js/crates/next-api/src/project_asset_hashes_manifest.rs
project_asset_hashes_manifest.rs209 lines6.1 KB
use anyhow::Result;
use serde::{
    Serializer,
    ser::{Error, SerializeMap},
};
use turbo_rcstr::RcStr;
use turbo_tasks::{FxIndexSet, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
use turbo_tasks_fs::{File, FileContent, FileSystemPath};
use turbo_tasks_hash::HashAlgorithm;
use turbopack_core::{
    asset::{Asset, AssetContent},
    output::{
        ExpandOutputAssetsInput, OutputAsset, OutputAssets, OutputAssetsReference,
        expand_output_assets,
    },
};

use crate::{
    project::Project,
    route::{Endpoint, EndpointGroup, Endpoints},
};

#[turbo_tasks::value]
struct AssetHashesManifestAsset {
    output_path: FileSystemPath,
    project: ResolvedVc<Project>,
    asset_root: FileSystemPath,
}

#[turbo_tasks::value_impl]
impl AssetHashesManifestAsset {
    #[turbo_tasks::function]
    pub fn new(
        output_path: FileSystemPath,
        project: ResolvedVc<Project>,
        asset_root: FileSystemPath,
    ) -> Vc<Self> {
        Self {
            output_path,
            project,
            asset_root,
        }
        .cell()
    }
}

#[turbo_tasks::value_impl]
impl OutputAssetsReference for AssetHashesManifestAsset {}

#[turbo_tasks::value_impl]
impl OutputAsset for AssetHashesManifestAsset {
    #[turbo_tasks::function]
    async fn path(&self) -> Vc<FileSystemPath> {
        self.output_path.clone().cell()
    }
}

#[turbo_tasks::function]
pub async fn endpoint_outputs(endpoint: Vc<Box<dyn Endpoint>>) -> Result<Vc<OutputAssets>> {
    Ok(*endpoint.output().await?.output_assets)
}

#[turbo_tasks::function]
pub async fn endpoints_outputs(endpoints: Vc<Endpoints>) -> Result<Vc<OutputAssets>> {
    let endpoints = endpoints.await?;
    let all_outputs = endpoints
        .iter()
        .map(async |endpoint| endpoint.output().await?.output_assets.await)
        .try_join()
        .await?;
    let set = all_outputs
        .into_iter()
        .flatten()
        .copied()
        .collect::<FxIndexSet<_>>();
    Ok(Vc::cell(set.into_iter().collect()))
}

#[turbo_tasks::value(transparent)]
pub struct OutputAssetsWithPaths(Vec<(ResolvedVc<Box<dyn OutputAsset>>, RcStr)>);

#[turbo_tasks::function]
pub async fn expand_outputs(
    project: Vc<Project>,
    root: FileSystemPath,
) -> Result<Vc<OutputAssetsWithPaths>> {
    let entrypoint_groups = project.get_all_endpoint_groups(false).await?;

    let output_assets = entrypoint_groups
        .iter()
        .map(|(_, EndpointGroup { primary, .. })| {
            if let &[entry] = &primary.as_slice() {
                endpoint_outputs(*entry.endpoint)
            } else {
                let endpoints = Vc::cell(primary.iter().map(|entry| entry.endpoint).collect());
                endpoints_outputs(endpoints)
            }
        })
        .collect::<Vec<_>>();

    let output_assets = expand_output_assets(
        output_assets
            .iter()
            .try_join()
            .await?
            .into_iter()
            .flatten()
            .map(|asset| ExpandOutputAssetsInput::Asset(*asset)),
        true,
    )
    .await?;

    let mut output_assets = output_assets
        .into_iter()
        .map(async |asset| {
            if let Some(path) = root.get_path_to(&*asset.path().await?) {
                Ok(Some((asset, RcStr::from(path))))
            } else {
                Ok(None)
            }
        })
        .try_flat_join()
        .await?;

    // Shared JS assets aren't duplicated here, but we have some duplicate OutputAssets with the
    // same path, e.g. a static image which exists twice, once with the server and then also with
    // the client chunking context.
    output_assets.sort_unstable_by(|(_, a), (_, b)| a.cmp(b));
    output_assets.dedup_by(|(_, a), (_, b)| a == b);

    Ok(Vc::cell(output_assets))
}

#[turbo_tasks::value_impl]
impl Asset for AssetHashesManifestAsset {
    #[turbo_tasks::function(root)]
    async fn content(&self) -> Result<Vc<AssetContent>> {
        let output_assets = expand_outputs(*self.project, self.asset_root.clone()).await?;

        let asset_paths = output_assets
            .into_iter()
            .map(async |(asset, path)| {
                Ok((
                    path,
                    asset
                        .content_hash(
                            self.project.next_config().output_hash_salt(),
                            HashAlgorithm::Xxh3Hash128Base38,
                        )
                        .await?,
                ))
            })
            .try_join()
            .await?;

        struct Manifest<'a> {
            asset_paths: &'a Vec<(&'a RcStr, ReadRef<Option<RcStr>>)>,
        }

        impl serde::Serialize for Manifest<'_> {
            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
                let mut map = serializer.serialize_map(Some(self.asset_paths.len()))?;
                for (path, content_hash) in self.asset_paths {
                    map.serialize_entry(
                        path,
                        if let Some(content_hash) = content_hash.as_ref() {
                            content_hash
                        } else {
                            return Err(S::Error::custom("asset content hash failed"));
                        },
                    )?;
                }
                map.end()
            }
        }

        let json = serde_json::to_string(&Manifest {
            asset_paths: &asset_paths,
        })?;

        Ok(AssetContent::file(
            FileContent::Content(File::from(json)).cell(),
        ))
    }
}

#[turbo_tasks::function]
pub async fn immutable_hashes_manifest_asset_if_enabled(
    project: ResolvedVc<Project>,
) -> Result<Vc<OutputAssets>> {
    if *project.next_config().enable_immutable_assets().await? {
        let path = project
            .node_root()
            .await?
            .join("immutable-static-hashes.json")?;

        let asset = AssetHashesManifestAsset::new(
            path,
            *project,
            project.client_relative_path().owned().await?,
        )
        .to_resolved()
        .await?;
        Ok(Vc::cell(vec![ResolvedVc::upcast(asset)]))
    } else {
        Ok(OutputAssets::empty())
    }
}
Quest for Codev2.0.0
/
SIGN IN