next.js/crates/next-core/src/emit.rs
emit.rs329 lines10.8 KB
use anyhow::{Ok, Result};
use async_trait::async_trait;
use futures::join;
use smallvec::{SmallVec, smallvec};
use tracing::Instrument;
use turbo_rcstr::RcStr;
use turbo_tasks::{
    FxIndexMap, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToStringRef, Vc,
};
use turbo_tasks_fs::{FileContent, FileSystemPath, rebase};
use turbo_tasks_hash::{encode_hex, hash_xxh3_hash64};
use turbopack_core::{
    asset::{Asset, AssetContent},
    issue::{Issue, IssueExt, IssueSeverity, IssueStage, StyledString},
    output::{ExpandedOutputAssets, OutputAsset, OutputAssets},
    reference::all_assets_from_entries,
};

/// Emits all assets transitively reachable from the given chunks, that are
/// inside the node root or the client root.
///
/// Assets inside the given client root are rebased to the given client output
/// path.
#[turbo_tasks::function]
pub async fn emit_all_assets(
    assets: Vc<OutputAssets>,
    node_root: FileSystemPath,
    client_relative_path: FileSystemPath,
    client_output_path: FileSystemPath,
) -> Result<()> {
    emit_assets(
        all_assets_from_entries(assets),
        node_root,
        client_relative_path,
        client_output_path,
    )
    .as_side_effect()
    .await?;
    Ok(())
}

/// Emits all assets transitively reachable from the given chunks, that are
/// inside the node root or the client root.
///
/// Assets inside the given client root are rebased to the given client output
/// path.
#[turbo_tasks::function]
pub async fn emit_assets(
    assets: Vc<ExpandedOutputAssets>,
    node_root: FileSystemPath,
    client_relative_path: FileSystemPath,
    client_output_path: FileSystemPath,
) -> Result<()> {
    enum Location {
        Node,
        Client,
    }
    let assets = assets
        .await?
        .iter()
        .copied()
        .map(async |asset| {
            let path = asset.path().owned().await?;
            let location = if path.is_inside_ref(&node_root) {
                Location::Node
            } else if path.is_inside_ref(&client_relative_path) {
                Location::Client
            } else {
                return Ok(None);
            };
            Ok(Some((location, path, asset)))
        })
        .try_flat_join()
        .await?;

    type AssetVec = SmallVec<[ResolvedVc<Box<dyn OutputAsset>>; 1]>;
    let mut node_assets_by_path: FxIndexMap<FileSystemPath, AssetVec> = FxIndexMap::default();
    let mut client_assets_by_path: FxIndexMap<FileSystemPath, AssetVec> = FxIndexMap::default();
    for (location, path, asset) in assets {
        match location {
            Location::Node => {
                node_assets_by_path
                    .entry(path)
                    .or_insert_with(|| smallvec![])
                    .push(asset);
            }
            Location::Client => {
                client_assets_by_path
                    .entry(path)
                    .or_insert_with(|| smallvec![])
                    .push(asset);
            }
        }
    }

    /// Checks for duplicate assets at the same path. If duplicates with
    /// different content are found, emits an `EmitConflictIssue` for each
    /// conflict but still returns the first asset so emission can continue.
    async fn check_duplicates(
        path: &FileSystemPath,
        assets: AssetVec,
        node_root: &FileSystemPath,
    ) -> Result<ResolvedVc<Box<dyn OutputAsset>>> {
        let mut iter = assets.into_iter();
        let first = iter.next().unwrap();
        for next in iter {
            let ext: RcStr = path.extension().unwrap_or_default().into();
            if let Some(detail) = assets_diff(*next, *first, ext, node_root.clone())
                .owned()
                .await?
            {
                EmitConflictIssue {
                    asset_path: path.clone(),
                    detail,
                }
                .resolved_cell()
                .emit();
            }
        }
        Ok(first)
    }

    // Use join! instead of try_join! to collect all errors deterministically
    // rather than returning whichever branch fails first non-deterministically.
    let (node_result, client_result) = join!(
        node_assets_by_path
            .into_iter()
            .map(|(path, assets)| {
                let node_root = node_root.clone();

                async move {
                    let asset = check_duplicates(&path, assets, &node_root).await?;
                    let span = tracing::info_span!(
                        "emit asset",
                        name = %path.to_string_ref().await?
                    );
                    async move { emit(*asset).as_side_effect().await }
                        .instrument(span)
                        .await
                }
            })
            .try_join(),
        client_assets_by_path
            .into_iter()
            .map(|(path, assets)| {
                let node_root = node_root.clone();
                let client_relative_path = client_relative_path.clone();
                let client_output_path = client_output_path.clone();

                async move {
                    let asset = check_duplicates(&path, assets, &node_root).await?;
                    let span = tracing::info_span!(
                        "emit asset",
                        name = %path.to_string_ref().await?
                    );
                    async move {
                        // Client assets are emitted to the client output path, which is
                        // prefixed with _next. We need to rebase them to
                        // remove that prefix.
                        emit_rebase(*asset, client_relative_path, client_output_path)
                            .as_side_effect()
                            .await
                    }
                    .instrument(span)
                    .await
                }
            })
            .try_join(),
    );
    node_result?;
    client_result?;
    Ok(())
}

#[turbo_tasks::function]
async fn emit(asset: Vc<Box<dyn OutputAsset>>) -> Result<()> {
    asset
        .content()
        .to_resolved()
        .await?
        .write(asset.path().owned().await?)
        .as_side_effect()
        .await?;
    Ok(())
}

#[turbo_tasks::function]
async fn emit_rebase(
    asset: Vc<Box<dyn OutputAsset>>,
    from: FileSystemPath,
    to: FileSystemPath,
) -> Result<()> {
    let path = rebase(asset.path().owned().await?, from, to)
        .owned()
        .await?;
    let content = asset.content();
    content
        .to_resolved()
        .await?
        .write(path)
        .as_side_effect()
        .await?;
    Ok(())
}

/// Compares two assets that target the same output path. If their content
/// differs, writes both versions under `node_root` as `<hash>.<ext>` and
/// returns a description of the difference.
#[turbo_tasks::function]
async fn assets_diff(
    asset1: Vc<Box<dyn OutputAsset>>,
    asset2: Vc<Box<dyn OutputAsset>>,
    extension: RcStr,
    node_root: FileSystemPath,
) -> Result<Vc<Option<RcStr>>> {
    let content1 = asset1.content().await?;
    let content2 = asset2.content().await?;

    let detail = match (&*content1, &*content2) {
        (AssetContent::File(content1), AssetContent::File(content2)) => {
            let content1 = content1.await?;
            let content2 = content2.await?;

            match (&*content1, &*content2) {
                (FileContent::NotFound, FileContent::NotFound) => None,
                (FileContent::Content(file1), FileContent::Content(file2)) => {
                    if file1 == file2 {
                        None
                    } else {
                        // Write both versions under node_root as <hash>.<ext> so the
                        // user can diff them.
                        let ext = &*extension;
                        let hash1 = encode_hex(hash_xxh3_hash64(file1.content().content_hash()));
                        let hash2 = encode_hex(hash_xxh3_hash64(file2.content().content_hash()));
                        let name1 = if ext.is_empty() {
                            hash1
                        } else {
                            format!("{hash1}.{ext}")
                        };
                        let name2 = if ext.is_empty() {
                            hash2
                        } else {
                            format!("{hash2}.{ext}")
                        };
                        let path1 = node_root.join(&name1)?;
                        let path2 = node_root.join(&name2)?;
                        path1
                            .write(FileContent::Content(file1.clone()).cell())
                            .as_side_effect()
                            .await?;
                        path2
                            .write(FileContent::Content(file2.clone()).cell())
                            .as_side_effect()
                            .await?;
                        Some(format!(
                            "file content differs, written to:\n  {}\n  {}",
                            path1.to_string_ref().await?,
                            path2.to_string_ref().await?,
                        ))
                    }
                }
                _ => Some(
                    "assets at the same path have mismatched file content types (one task wants \
                     to write the file, another wants to delete it)"
                        .into(),
                ),
            }
        }
        (
            AssetContent::Redirect {
                target: target1,
                link_type: link_type1,
            },
            AssetContent::Redirect {
                target: target2,
                link_type: link_type2,
            },
        ) => {
            if target1 == target2 && link_type1 == link_type2 {
                None
            } else {
                Some(format!(
                    "assets at the same path are both redirects but point to different targets: \
                     {target1} vs {target2}"
                ))
            }
        }
        _ => Some(
            "assets at the same path have different content types (one is a file, the other is a \
             redirect)"
                .into(),
        ),
    };

    Ok(Vc::cell(detail.map(|d| d.into())))
}

#[turbo_tasks::value]
struct EmitConflictIssue {
    asset_path: FileSystemPath,
    detail: RcStr,
}

#[async_trait]
#[turbo_tasks::value_impl]
impl Issue for EmitConflictIssue {
    async fn file_path(&self) -> Result<FileSystemPath> {
        Ok(self.asset_path.clone())
    }

    fn stage(&self) -> IssueStage {
        IssueStage::Emit
    }

    fn severity(&self) -> IssueSeverity {
        IssueSeverity::Error
    }

    async fn title(&self) -> Result<StyledString> {
        Ok(StyledString::Text(
            "Two or more assets with different content were emitted to the same output path".into(),
        ))
    }

    async fn description(&self) -> Result<Option<StyledString>> {
        Ok(Some(StyledString::Text(self.detail.clone())))
    }
}
Quest for Codev2.0.0
/
SIGN IN