next.js/crates/next-api/src/next_server_nft.rs
next_server_nft.rs367 lines13.4 KB
use std::collections::BTreeSet;

use anyhow::{Context, Result, bail};
use bincode::{Decode, Encode};
use either::Either;
use next_core::{get_next_package, next_server::get_tracing_compile_time_info};
use serde_json::{Value, json};
use turbo_rcstr::RcStr;
use turbo_tasks::{
    NonLocalValue, ResolvedVc, TaskInput, TryFlatJoinIterExt, TryJoinIterExt, Vc,
    trace::TraceRawVcs,
};
use turbo_tasks_fs::{
    DirectoryContent, DirectoryEntry, File, FileContent, FileSystemPath, glob::Glob,
};
use turbopack::externals_tracing_module_context;
use turbopack_core::{
    asset::{Asset, AssetContent},
    context::AssetContext,
    file_source::FileSource,
    output::{OutputAsset, OutputAssets, OutputAssetsReference},
    reference_type::{CommonJsReferenceSubType, ReferenceType},
    resolve::{ResolveErrorMode, origin::PlainResolveOrigin, parse::Request},
    traced_asset::TracedAsset,
};
use turbopack_resolve::ecmascript::cjs_resolve;

use crate::{
    nft_json::{all_assets_from_entries_filtered, relativize_glob},
    project::Project,
};

#[derive(
    PartialEq, Eq, TraceRawVcs, NonLocalValue, Debug, Clone, Hash, TaskInput, Encode, Decode,
)]
enum ServerNftType {
    Minimal,
    Full,
}

#[turbo_tasks::function]
pub async fn next_server_nft_assets(project: Vc<Project>) -> Result<Vc<OutputAssets>> {
    let has_next_support = *project.ci_has_next_support().await?;
    let is_standalone = *project.next_config().is_standalone().await?;

    let minimal = ResolvedVc::upcast(
        ServerNftJsonAsset::new(project, ServerNftType::Minimal)
            .to_resolved()
            .await?,
    );

    if has_next_support && !is_standalone {
        // When deploying to Vercel, we only need next-minimal-server.js.nft.json
        Ok(Vc::cell(vec![minimal]))
    } else {
        Ok(Vc::cell(vec![
            minimal,
            ResolvedVc::upcast(
                ServerNftJsonAsset::new(project, ServerNftType::Full)
                    .to_resolved()
                    .await?,
            ),
        ]))
    }
}

#[turbo_tasks::value]
pub struct ServerNftJsonAsset {
    project: ResolvedVc<Project>,
    ty: ServerNftType,
}

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

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

#[turbo_tasks::value_impl]
impl OutputAsset for ServerNftJsonAsset {
    #[turbo_tasks::function]
    async fn path(&self) -> Result<Vc<FileSystemPath>> {
        let name = match self.ty {
            ServerNftType::Minimal => "next-minimal-server.js.nft.json",
            ServerNftType::Full => "next-server.js.nft.json",
        };

        Ok(self.project.node_root().await?.join(name)?.cell())
    }
}

#[turbo_tasks::value_impl]
impl Asset for ServerNftJsonAsset {
    #[turbo_tasks::function]
    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
        let this = self.await?;
        // Example: [project]/apps/my-website/.next/
        let base_dir = this
            .project
            .project_root_path()
            .await?
            .join(&this.project.node_root().await?.path)?;

        let mut server_output_assets =
            all_assets_from_entries_filtered(self.entries(), None, Some(self.ignores()))
                .await?
                .iter()
                .map(async |m| {
                    base_dir
                        .get_relative_path_to(&*m.path().await?)
                        .context("failed to compute relative path for server NFT JSON")
                })
                .try_join()
                .await?;

        // A few hardcoded files (not recursive)
        server_output_assets.push("./package.json".into());

        let next_dir = get_next_package(this.project.project_path().owned().await?).await?;
        for ty in ["app-page", "pages"] {
            let dir = next_dir.join(&format!("dist/server/route-modules/{ty}"))?;
            let module_path = dir.join("module.compiled.js")?;
            server_output_assets.push(
                base_dir
                    .get_relative_path_to(&module_path)
                    .context("failed to compute relative path for server NFT JSON")?,
            );

            let contexts_dir = dir.join("vendored/contexts")?;
            let DirectoryContent::Entries(contexts_files) = &*contexts_dir.read_dir().await? else {
                bail!(
                    "Expected contexts directory to be a directory, found: {:?}",
                    contexts_dir
                );
            };
            for (_, entry) in contexts_files {
                let DirectoryEntry::File(file) = entry else {
                    continue;
                };
                if file.extension() == Some("js") {
                    server_output_assets.push(
                        base_dir
                            .get_relative_path_to(file)
                            .context("failed to compute relative path for server NFT JSON")?,
                    )
                }
            }
        }

        server_output_assets.sort();
        // Dedupe as some entries may be duplicates: a file might be referenced multiple times,
        // e.g. as a RawModule (from an FS operation) and as an EcmascriptModuleAsset because it
        // was required.
        server_output_assets.dedup();

        let json = json!({
          "version": 1,
          "files": server_output_assets
        });

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

#[turbo_tasks::value_impl]
impl ServerNftJsonAsset {
    #[turbo_tasks::function]
    async fn entries(&self) -> Result<Vc<OutputAssets>> {
        let is_standalone = *self.project.next_config().is_standalone().await?;

        let asset_context = Vc::upcast(externals_tracing_module_context(
            get_tracing_compile_time_info(),
            false,
        ));

        let project_path = self.project.project_path().owned().await?;

        let next_resolve_origin = Vc::upcast(PlainResolveOrigin::new(
            asset_context,
            get_next_package(project_path.clone()).await?.join("_")?,
        ));

        let cache_handler = self
            .project
            .next_config()
            .cache_handler(project_path.clone())
            .await?;
        let cache_handlers = self
            .project
            .next_config()
            .cache_handlers(project_path.clone())
            .await?;

        // These are used by packages/next/src/server/require-hook.ts
        let shared_entries = ["styled-jsx", "styled-jsx/style", "styled-jsx/style.js"];

        let cache_handler_entries = cache_handler.into_iter().chain(cache_handlers).map(|f| {
            asset_context
                .process(
                    Vc::upcast(FileSource::new(f.clone())),
                    ReferenceType::CommonJs(CommonJsReferenceSubType::Undefined),
                )
                .module()
        });

        let entries = match self.ty {
            ServerNftType::Full => Either::Left(
                if is_standalone {
                    Either::Left(
                        [
                            "next/dist/server/lib/start-server",
                            "next/dist/server/next",
                            "next/dist/server/require-hook",
                        ]
                        .into_iter(),
                    )
                } else {
                    Either::Right(std::iter::empty())
                }
                .chain(std::iter::once("next/dist/server/next-server")),
            ),
            ServerNftType::Minimal => Either::Right(std::iter::once(
                "next/dist/compiled/next-server/server.runtime.prod",
            )),
        };

        Ok(Vc::cell(
            cache_handler_entries
                .chain(
                    shared_entries
                        .into_iter()
                        .chain(entries)
                        .map(async |path| {
                            Ok(cjs_resolve(
                                next_resolve_origin,
                                Request::parse_string(path.into()),
                                CommonJsReferenceSubType::Undefined,
                                None,
                                ResolveErrorMode::Error,
                            )
                            .primary_modules()
                            .await?
                            .into_iter()
                            .map(|m| **m))
                        })
                        .try_flat_join()
                        .await?,
                )
                .map(|m| Vc::upcast::<Box<dyn OutputAsset>>(TracedAsset::new(m)).to_resolved())
                .try_join()
                .await?,
        ))
    }

    #[turbo_tasks::function]
    async fn ignores(&self) -> Result<Vc<Glob>> {
        let is_standalone = *self.project.next_config().is_standalone().await?;
        let has_next_support = *self.project.ci_has_next_support().await?;
        let project_path = self.project.project_path().owned().await?;

        let output_file_tracing_excludes = self
            .project
            .next_config()
            .output_file_tracing_excludes()
            .await?;
        let mut additional_ignores = BTreeSet::new();
        if let Some(output_file_tracing_excludes) = output_file_tracing_excludes
            .as_ref()
            .and_then(Value::as_object)
        {
            for (glob_pattern, exclude_patterns) in output_file_tracing_excludes {
                // Check if the route matches the glob pattern
                let glob = Glob::new(RcStr::from(glob_pattern.clone()), Default::default()).await?;
                if glob.matches("next-server")
                    && let Some(patterns) = exclude_patterns.as_array()
                {
                    for pattern in patterns {
                        if let Some(pattern_str) = pattern.as_str() {
                            let (glob, root) = relativize_glob(pattern_str, project_path.clone())?;
                            let glob = if root.path.is_empty() {
                                glob.to_string()
                            } else {
                                format!("{root}/{glob}")
                            };
                            additional_ignores.insert(glob);
                        }
                    }
                }
            }
        }

        let server_ignores_glob = [
            "**/node_modules/react{,-dom,-server-dom-turbopack}/**/*.development.js",
            "**/*.d.ts",
            "**/*.map",
            "**/next/dist/pages/**/*",
            "**/next/dist/compiled/next-server/**/*.dev.js",
            "**/next/dist/compiled/webpack/*",
            "**/node_modules/webpack5/**/*",
            "**/next/dist/server/lib/route-resolver*",
            "**/next/dist/compiled/semver/semver/**/*.js",
            "**/next/dist/compiled/jest-worker/**/*",
            // -- The following were added for Turbopack specifically --
            // client/components/use-action-queue.ts has a process.env.NODE_ENV guard, but we can't set that due to React: https://github.com/vercel/next.js/pull/75254
            "**/next/dist/next-devtools/userspace/use-app-dev-rendering-indicator.js",
            // client/components/app-router.js has a process.env.NODE_ENV guard, but we
            // can't set that.
            "**/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js",
            // server/lib/router-server.js doesn't guard this require:
            "**/next/dist/server/lib/router-utils/setup-dev-bundler.js",
            // server/next.js doesn't guard this require
            "**/next/dist/server/dev/next-dev-server.js",
            // next/dist/compiled/babel* pulls in this, but we never actually transpile at
            // deploy-time
            "**/next/dist/compiled/browserslist/**",
        ]
        .into_iter()
        .chain(additional_ignores.iter().map(|s| s.as_str()))
        // only ignore image-optimizer code when
        // this is being handled outside of next-server
        .chain(if has_next_support {
            Either::Left(
                [
                    "**/node_modules/sharp/**/*",
                    "**/@img/sharp-libvips*/**/*",
                    "**/next/dist/server/image-optimizer.js",
                ]
                .into_iter(),
            )
        } else {
            Either::Right(std::iter::empty())
        })
        .chain(if is_standalone {
            Either::Left(std::iter::empty())
        } else {
            Either::Right(["**/*/next/dist/server/next.js", "**/*/next/dist/bin/next"].into_iter())
        })
        .map(|g| Glob::new(g.into(), Default::default()))
        .collect::<Vec<_>>();

        Ok(match self.ty {
            ServerNftType::Full => Glob::alternatives(server_ignores_glob),
            ServerNftType::Minimal => Glob::alternatives(
                server_ignores_glob
                    .into_iter()
                    .chain(
                        [
                            "**/next/dist/compiled/edge-runtime/**/*",
                            "**/next/dist/server/web/sandbox/**/*",
                            "**/next/dist/server/post-process.js",
                        ]
                        .into_iter()
                        .map(|g| Glob::new(g.into(), Default::default())),
                    )
                    .collect(),
            ),
        })
    }
}
Quest for Codev2.0.0
/
SIGN IN