next.js/crates/next-api/src/routes_hashes_manifest.rs
routes_hashes_manifest.rs294 lines8.8 KB
use anyhow::Result;
use serde::Serialize;
use turbo_rcstr::RcStr;
use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
use turbo_tasks_fs::{FileContent, FileSystemPath};
use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher};
use turbopack_core::{
    asset::{Asset, AssetContent},
    module::{Module, Modules},
    module_graph::{GraphTraversalAction, ModuleGraph},
    output::{
        ExpandOutputAssetsInput, OutputAsset, OutputAssets, OutputAssetsReference,
        expand_output_assets,
    },
};

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

#[turbo_tasks::value(shared)]
pub struct EndpointHashes {
    pub sources_hash: u64,
    pub outputs_hash: u64,
}

impl EndpointHashes {
    pub fn merge<'l>(iterator: impl Iterator<Item = (Option<RcStr>, &'l EndpointHashes)>) -> Self {
        let mut sources_hasher = Xxh3Hash64Hasher::new();
        let mut outputs_hasher = Xxh3Hash64Hasher::new();

        for (key, hashes) in iterator {
            key.deterministic_hash(&mut sources_hasher);
            key.deterministic_hash(&mut outputs_hasher);
            hashes.sources_hash.deterministic_hash(&mut sources_hasher);
            hashes.outputs_hash.deterministic_hash(&mut outputs_hasher);
        }

        Self {
            sources_hash: sources_hasher.finish(),
            outputs_hash: outputs_hasher.finish(),
        }
    }
}

#[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| Ok(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::function]
pub async fn outputs_hash(outputs: Vc<OutputAssets>) -> Result<Vc<u64>> {
    let output_assets = expand_output_assets(
        outputs
            .await?
            .into_iter()
            .map(|asset| ExpandOutputAssetsInput::Asset(*asset)),
        true,
    )
    .await?;
    let outputs_hashes = output_assets
        .iter()
        .map(|asset| asset.content().hash())
        .try_join()
        .await?;

    let outputs_hash = {
        let mut hasher = Xxh3Hash64Hasher::new();
        for hash in outputs_hashes.iter() {
            hash.deterministic_hash(&mut hasher);
        }
        hasher.finish()
    };

    Ok(Vc::cell(outputs_hash))
}

#[turbo_tasks::function]
pub async fn endpoint_entry_modules(
    base_module_graph: Vc<ModuleGraph>,
    endpoint: Vc<Box<dyn Endpoint>>,
) -> Result<Vc<Modules>> {
    let entries = endpoint.entries();
    let additional_entries = endpoint.additional_entries(base_module_graph);
    let modules = entries
        .await?
        .into_iter()
        .chain(additional_entries.await?)
        .flat_map(|e| e.entries())
        .collect::<FxIndexSet<_>>();
    Ok(Vc::cell(modules.into_iter().collect()))
}

#[turbo_tasks::function]
pub async fn endpoints_entry_modules(
    base_module_graph: Vc<ModuleGraph>,
    endpoints: Vc<Endpoints>,
) -> Result<Vc<Modules>> {
    let endpoints = endpoints.await?;
    let entries_and_additional_entries = endpoints
        .iter()
        .map(async |endpoint| {
            let entries = endpoint.entries();
            let additional_entries = endpoint.additional_entries(base_module_graph);
            Ok((entries.await?, additional_entries.await?))
        })
        .try_join()
        .await?;
    let modules = entries_and_additional_entries
        .into_iter()
        .flat_map(|(entries, additional_entries)| {
            entries
                .into_iter()
                .chain(additional_entries)
                .flat_map(|e| e.entries())
        })
        .collect::<FxIndexSet<_>>();
    Ok(Vc::cell(modules.into_iter().collect()))
}

#[turbo_tasks::function]
pub async fn sources_hash(module_graph: Vc<ModuleGraph>, modules: Vc<Modules>) -> Result<Vc<u64>> {
    let modules = modules.await?;

    let mut all_modules = FxIndexSet::default();

    let module_graph = module_graph.await?;

    module_graph.traverse_nodes_dfs(
        modules.into_iter().copied(),
        &mut all_modules,
        |module, all_modules| {
            all_modules.insert(*module);
            Ok(GraphTraversalAction::Continue)
        },
        |_, _| Ok(()),
    )?;

    let sources = all_modules
        .iter()
        .map(|module| module.source())
        .try_flat_join()
        .await?
        .into_iter()
        .map(|source| source.content().hash())
        .try_join()
        .await?;

    let sources_hash = {
        let mut hasher = Xxh3Hash64Hasher::new();
        for source in sources.iter() {
            source.deterministic_hash(&mut hasher);
        }
        hasher.finish()
    };

    Ok(Vc::cell(sources_hash))
}

#[derive(Serialize)]
struct RoutesHashesManifest<'l> {
    pub routes: FxIndexMap<&'l str, EndpointHashStrings>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EndpointHashStrings {
    pub sources_hash: String,
    pub outputs_hash: String,
}

#[turbo_tasks::value]
pub struct RoutesHashesManifestAsset {
    path: FileSystemPath,
    project: ResolvedVc<Project>,
}

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

#[turbo_tasks::value_impl]
impl Asset for RoutesHashesManifestAsset {
    #[turbo_tasks::function]
    async fn content(&self) -> Result<Vc<AssetContent>> {
        let module_graphs = self.project.whole_app_module_graphs().await?;
        let base_module_graph = *module_graphs.base;
        let full_module_graph = *module_graphs.full;

        let mut entrypoint_hashes = FxIndexMap::default();

        let entrypoint_groups = self.project.get_all_endpoint_groups(false).await?;

        for (key, EndpointGroup { primary, .. }) in entrypoint_groups {
            let entry = if let &[entry] = &primary.as_slice() {
                (
                    sources_hash(
                        full_module_graph,
                        endpoint_entry_modules(base_module_graph, *entry.endpoint),
                    ),
                    outputs_hash(endpoint_outputs(*entry.endpoint)),
                )
            } else {
                let endpoints = Vc::cell(primary.iter().map(|entry| entry.endpoint).collect());
                (
                    sources_hash(
                        full_module_graph,
                        endpoints_entry_modules(base_module_graph, endpoints),
                    ),
                    outputs_hash(endpoints_outputs(endpoints)),
                )
            };
            entrypoint_hashes.insert(key.as_str(), entry);
        }

        let entrypoint_hashes_values = entrypoint_hashes
            .values()
            .map(async |(sources_hash, outputs_hash)| {
                Ok((sources_hash.await?, outputs_hash.await?))
            })
            .try_join()
            .await?;

        let manifest = serde_json::to_string_pretty(&RoutesHashesManifest {
            routes: entrypoint_hashes
                .into_keys()
                .zip(entrypoint_hashes_values)
                .map(|(k, (sources_hash, outputs_hash))| {
                    (
                        k,
                        EndpointHashStrings {
                            sources_hash: format!("{:016x}", *sources_hash),
                            outputs_hash: format!("{:016x}", *outputs_hash),
                        },
                    )
                })
                .collect(),
        })?;
        Ok(AssetContent::File(FileContent::Content(manifest.into()).resolved_cell()).cell())
    }
}

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

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

#[turbo_tasks::function]
pub async fn routes_hashes_manifest_asset_if_enabled(
    project: ResolvedVc<Project>,
) -> Result<Vc<OutputAssets>> {
    let should_write = *project.should_write_routes_hashes_manifest().await?;
    let assets = if should_write {
        let path = project
            .node_root()
            .await?
            .join("diagnostics/routes-hashes-manifest.json")?;
        let asset = RoutesHashesManifestAsset::new(path, *project)
            .to_resolved()
            .await?;
        vec![ResolvedVc::upcast(asset)]
    } else {
        vec![]
    };
    Ok(Vc::cell(assets))
}
Quest for Codev2.0.0
/
SIGN IN