next.js/crates/next-core/src/next_manifests/mod.rs
mod.rs499 lines13.8 KB
//! Type definitions for the Next.js manifest formats.

pub mod client_reference_manifest;
mod encode_uri_component;

use anyhow::{Context, Result};
use bincode::{Decode, Encode};
use serde::{Deserialize, Serialize};
use turbo_rcstr::RcStr;
use turbo_tasks::{
    FxIndexMap, NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryFlatJoinIterExt, TryJoinIterExt,
    Vc, trace::TraceRawVcs,
};
use turbo_tasks_fs::{File, FileContent, FileSystemPath};
use turbopack_core::{
    asset::{Asset, AssetContent},
    output::{OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsWithReferenced},
};

use crate::next_config::RouteHas;

#[derive(Serialize, Default, Debug)]
pub struct PagesManifest {
    #[serde(flatten)]
    pub pages: FxIndexMap<RcStr, RcStr>,
}

#[derive(Debug)]
#[turbo_tasks::value(shared)]
pub struct BuildManifest {
    pub output_path: FileSystemPath,
    pub client_relative_path: FileSystemPath,

    pub polyfill_files: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
    pub root_main_files: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
    #[bincode(with = "turbo_bincode::indexmap")]
    pub pages: FxIndexMap<RcStr, ResolvedVc<OutputAssets>>,
}

#[turbo_tasks::value_impl]
impl OutputAssetsReference for BuildManifest {
    #[turbo_tasks::function]
    async fn references(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
        let chunks: Vec<ReadRef<OutputAssets>> = self.pages.values().try_join().await?;

        let root_main_files = self
            .root_main_files
            .iter()
            .map(async |c| Ok(c.path().await?.has_extension(".js").then_some(*c)))
            .try_flat_join()
            .await?;

        let references = chunks
            .into_iter()
            .flatten()
            .copied()
            .chain(root_main_files)
            .chain(self.polyfill_files.iter().copied())
            .collect();

        Ok(OutputAssetsWithReferenced::from_assets(Vc::cell(
            references,
        )))
    }
}

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

#[turbo_tasks::value_impl]
impl Asset for BuildManifest {
    #[turbo_tasks::function]
    async fn content(&self) -> Result<Vc<AssetContent>> {
        let client_relative_path = &self.client_relative_path;

        #[derive(Serialize, Default, Debug)]
        #[serde(rename_all = "camelCase")]
        pub struct SerializedBuildManifest {
            pub dev_files: Vec<RcStr>,
            pub amp_dev_files: Vec<RcStr>,
            pub polyfill_files: Vec<RcStr>,
            pub low_priority_files: Vec<RcStr>,
            pub root_main_files: Vec<RcStr>,
            pub pages: FxIndexMap<RcStr, Vec<RcStr>>,
            pub amp_first_pages: Vec<RcStr>,
        }

        let pages: Vec<(RcStr, Vec<RcStr>)> = self
            .pages
            .iter()
            .map(async |(k, chunks)| {
                Ok((
                    k.clone(),
                    chunks
                        .await?
                        .iter()
                        .copied()
                        .map(async |chunk| {
                            let chunk_path = chunk.path().await?;
                            Ok(client_relative_path
                                .get_path_to(&chunk_path)
                                .context("client chunk entry path must be inside the client root")?
                                .into())
                        })
                        .try_join()
                        .await?,
                ))
            })
            .try_join()
            .await?;

        let polyfill_files: Vec<RcStr> = self
            .polyfill_files
            .iter()
            .copied()
            .map(async |chunk| {
                let chunk_path = chunk.path().await?;
                Ok(client_relative_path
                    .get_path_to(&chunk_path)
                    .context("failed to resolve client-relative path to polyfill")?
                    .into())
            })
            .try_join()
            .await?;

        let root_main_files: Vec<RcStr> = self
            .root_main_files
            .iter()
            .map(async |chunk| {
                let chunk_path = chunk.path().await?;
                if !chunk_path.has_extension(".js") {
                    Ok(None)
                } else {
                    Ok(Some(
                        client_relative_path
                            .get_path_to(&chunk_path)
                            .context("failed to resolve client-relative path to root_main_file")?
                            .into(),
                    ))
                }
            })
            .try_flat_join()
            .await?;

        let manifest = SerializedBuildManifest {
            pages: FxIndexMap::from_iter(pages),
            polyfill_files,
            root_main_files,
            ..Default::default()
        };

        Ok(AssetContent::file(
            FileContent::Content(File::from(serde_json::to_string_pretty(&manifest)?)).cell(),
        ))
    }
}

#[derive(Debug)]
#[turbo_tasks::value(shared)]
pub struct ClientBuildManifest {
    pub output_path: FileSystemPath,
    pub client_relative_path: FileSystemPath,

    #[bincode(with = "turbo_bincode::indexmap")]
    pub pages: FxIndexMap<RcStr, ResolvedVc<Box<dyn OutputAsset>>>,
}

#[turbo_tasks::value_impl]
impl OutputAssetsReference for ClientBuildManifest {
    #[turbo_tasks::function]
    async fn references(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
        let chunks: Vec<ResolvedVc<Box<dyn OutputAsset>>> = self.pages.values().copied().collect();
        Ok(OutputAssetsWithReferenced::from_assets(Vc::cell(chunks)))
    }
}

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

#[turbo_tasks::value_impl]
impl Asset for ClientBuildManifest {
    #[turbo_tasks::function]
    async fn content(&self) -> Result<Vc<AssetContent>> {
        let client_relative_path = &self.client_relative_path;

        let manifest: FxIndexMap<RcStr, Vec<RcStr>> = self
            .pages
            .iter()
            .map(async |(k, chunk)| {
                Ok((
                    k.clone(),
                    vec![
                        client_relative_path
                            .get_path_to(&*chunk.path().await?)
                            .context("client chunk entry path must be inside the client root")?
                            .into(),
                    ],
                ))
            })
            .try_join()
            .await?
            .into_iter()
            .collect();

        Ok(AssetContent::file(
            FileContent::Content(File::from(serde_json::to_string_pretty(&manifest)?)).cell(),
        ))
    }
}

#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase", tag = "version")]
#[allow(clippy::large_enum_variant)]
pub enum MiddlewaresManifest {
    #[serde(rename = "2")]
    MiddlewaresManifestV2(MiddlewaresManifestV2),
    #[serde(other)]
    Unsupported,
}

impl Default for MiddlewaresManifest {
    fn default() -> Self {
        Self::MiddlewaresManifestV2(Default::default())
    }
}

#[derive(
    Debug,
    Clone,
    Hash,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    TaskInput,
    TraceRawVcs,
    Serialize,
    Deserialize,
    NonLocalValue,
    Encode,
    Decode,
)]
#[serde(rename_all = "camelCase", default)]
pub struct ProxyMatcher {
    // When skipped, next.js will fill the field during merging.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub regexp: Option<RcStr>,
    #[serde(skip_serializing_if = "bool_is_true")]
    pub locale: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub has: Option<Vec<RouteHas>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub missing: Option<Vec<RouteHas>>,
    pub original_source: RcStr,
}

impl Default for ProxyMatcher {
    fn default() -> Self {
        Self {
            regexp: None,
            locale: true,
            has: None,
            missing: None,
            original_source: Default::default(),
        }
    }
}

fn bool_is_true(b: &bool) -> bool {
    *b
}

#[derive(Serialize, Default, Debug)]
pub struct EdgeFunctionDefinition {
    pub files: Vec<RcStr>,
    pub name: RcStr,
    pub page: RcStr,
    pub entrypoint: RcStr,
    pub matchers: Vec<ProxyMatcher>,
    pub wasm: Vec<AssetBinding>,
    pub assets: Vec<AssetBinding>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub regions: Option<Regions>,
    pub env: FxIndexMap<RcStr, RcStr>,
}

#[derive(Serialize, Default, Debug)]
pub struct InstrumentationDefinition {
    pub files: Vec<RcStr>,
    pub name: RcStr,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub wasm: Vec<AssetBinding>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub assets: Vec<AssetBinding>,
}

#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AssetBinding {
    pub name: RcStr,
    pub file_path: RcStr,
}

#[derive(Serialize, Debug)]
#[serde(untagged)]
pub enum Regions {
    Multiple(Vec<RcStr>),
    Single(RcStr),
}

#[derive(Serialize, Default, Debug)]
pub struct MiddlewaresManifestV2 {
    pub sorted_middleware: Vec<RcStr>,
    pub middleware: FxIndexMap<RcStr, EdgeFunctionDefinition>,
    pub instrumentation: Option<InstrumentationDefinition>,
    pub functions: FxIndexMap<RcStr, EdgeFunctionDefinition>,
}

#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ReactLoadableManifest {
    #[serde(flatten)]
    pub manifest: FxIndexMap<RcStr, ReactLoadableManifestEntry>,
}

#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ReactLoadableManifestEntry {
    pub id: u32,
    pub files: Vec<RcStr>,
}

#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct NextFontManifest {
    pub pages: FxIndexMap<RcStr, Vec<RcStr>>,
    pub app: FxIndexMap<RcStr, Vec<RcStr>>,
    pub app_using_size_adjust: bool,
    pub pages_using_size_adjust: bool,
}

#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AppPathsManifest {
    #[serde(flatten)]
    pub edge_server_app_paths: PagesManifest,
    #[serde(flatten)]
    pub node_server_app_paths: PagesManifest,
}

// A struct represent a single entry in react-loadable-manifest.json.
// The manifest is in a format of:
// { [`${origin} -> ${imported}`]: { id: `${origin} -> ${imported}`, files:
// string[] } }
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LoadableManifest {
    pub id: ModuleId,
    pub files: Vec<RcStr>,
}

#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ServerReferenceManifest<'a> {
    /// A map from hashed action name to the runtime module we that exports it.
    pub node: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
    /// A map from hashed action name to the runtime module we that exports it.
    pub edge: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
}

#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ActionManifestEntry<'a> {
    /// A mapping from the page that uses the server action to the runtime
    /// module that exports it.
    pub workers: FxIndexMap<&'a str, ActionManifestWorkerEntry<'a>>,

    #[serde(rename = "exportedName")]
    pub exported_name: &'a str,

    pub filename: &'a str,

    /// Source location line number (1-indexed), if available
    #[serde(skip_serializing_if = "Option::is_none")]
    pub line: Option<u32>,

    /// Source location column number (1-indexed), if available
    #[serde(skip_serializing_if = "Option::is_none")]
    pub col: Option<u32>,
}

#[derive(Serialize, Debug)]
pub struct ActionManifestWorkerEntry<'a> {
    #[serde(rename = "moduleId")]
    pub module_id: ActionManifestModuleId<'a>,
    #[serde(rename = "async")]
    pub is_async: bool,
    #[serde(rename = "exportedName")]
    pub exported_name: &'a str,
    pub filename: &'a str,
}

#[derive(Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum ActionManifestModuleId<'a> {
    String(&'a str),
    Number(u64),
}

#[derive(
    Debug,
    Copy,
    Clone,
    Hash,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    TaskInput,
    TraceRawVcs,
    Serialize,
    Deserialize,
    NonLocalValue,
    Encode,
    Decode,
)]
#[serde(rename_all = "kebab-case")]
pub enum ActionLayer {
    Rsc,
    ActionBrowser,
}

#[derive(Serialize, Debug, Eq, PartialEq, Hash, Clone)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum ModuleId {
    String(RcStr),
    Number(u64),
}

#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FontManifest(pub Vec<FontManifestEntry>);

#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FontManifestEntry {
    pub url: RcStr,
    pub content: RcStr,
}

#[cfg(test)]
mod tests {
    use turbo_rcstr::rcstr;

    use super::*;

    #[test]
    fn test_middleware_matcher_serialization() {
        let matchers = vec![
            ProxyMatcher {
                regexp: None,
                locale: false,
                has: None,
                missing: None,
                original_source: rcstr!(""),
            },
            ProxyMatcher {
                regexp: Some(rcstr!(".*")),
                locale: true,
                has: Some(vec![RouteHas::Query {
                    key: rcstr!("foo"),
                    value: None,
                }]),
                missing: Some(vec![RouteHas::Query {
                    key: rcstr!("bar"),
                    value: Some(rcstr!("value")),
                }]),
                original_source: rcstr!("source"),
            },
        ];

        let serialized = serde_json::to_string(&matchers).unwrap();
        let deserialized: Vec<ProxyMatcher> = serde_json::from_str(&serialized).unwrap();

        assert_eq!(matchers, deserialized);
    }
}
Quest for Codev2.0.0
/
SIGN IN