next.js/crates/next-core/src/util.rs
util.rs560 lines19.3 KB
use std::{fmt::Display, str::FromStr};

use anyhow::{Result, bail};
use bincode::{Decode, Encode};
use next_taskless::{expand_next_js_template, expand_next_js_template_no_imports};
use serde::{Deserialize, de::DeserializeOwned};
use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::{
    FxIndexMap, NonLocalValue, TaskInput, Vc, fxindexset, trace::TraceRawVcs, turbobail,
};
use turbo_tasks_fs::{File, FileContent, FileJsonContent, FileSystem, FileSystemPath, rope::Rope};
use turbopack::module_options::RuleCondition;
use turbopack_core::{
    asset::AssetContent,
    compile_time_info::{
        CompileTimeDefineValue, CompileTimeDefines, DefinableNameSegment, FreeVarReference,
        FreeVarReferences,
    },
    condition::ContextCondition,
    issue::IssueSeverity,
    source::Source,
    virtual_source::VirtualSource,
};

use crate::{
    embed_js::next_js_fs, next_config::NextConfig, next_import_map::get_next_package,
    next_manifests::ProxyMatcher, next_shared::webpack_rules::WebpackLoaderBuiltinCondition,
};

const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates";

/// As opposed to [`EnvMap`], this map allows for `None` values, which means that the variables
/// should be replace with undefined.
#[turbo_tasks::value(transparent)]
pub struct OptionEnvMap(
    #[turbo_tasks(trace_ignore)]
    #[bincode(with = "turbo_bincode::indexmap")]
    FxIndexMap<RcStr, Option<RcStr>>,
);

pub fn defines(define_env: &FxIndexMap<RcStr, Option<RcStr>>) -> CompileTimeDefines {
    let mut defines = FxIndexMap::default();

    for (k, v) in define_env {
        defines
            .entry(
                k.split('.')
                    .map(|s| DefinableNameSegment::Name(s.into()))
                    .collect::<Vec<_>>(),
            )
            .or_insert_with(|| {
                if let Some(v) = v {
                    let val = serde_json::Value::from_str(v);
                    match val {
                        Ok(v) => v.into(),
                        _ => CompileTimeDefineValue::Evaluate(v.clone()),
                    }
                } else {
                    CompileTimeDefineValue::Undefined
                }
            });
    }

    CompileTimeDefines(defines)
}

/// Emits warnings or errors when inlining frequently changing Vercel system env vars
pub fn free_var_references_with_vercel_system_env_warnings(
    defines: CompileTimeDefines,
    severity: IssueSeverity,
) -> FreeVarReferences {
    // List of system env vars:
    //   not available as NEXT_PUBLIC_* anyway:
    //      CI
    //      VERCEL
    //      VERCEL_SKEW_PROTECTION_ENABLED
    //      VERCEL_AUTOMATION_BYPASS_SECRET
    //      VERCEL_GIT_PROVIDER
    //      VERCEL_GIT_REPO_SLUG
    //      VERCEL_GIT_REPO_OWNER
    //      VERCEL_GIT_REPO_ID
    //      VERCEL_OIDC_TOKEN
    //
    //   constant:
    //      VERCEL_PROJECT_PRODUCTION_URL
    //      VERCEL_REGION
    //      VERCEL_PROJECT_ID
    //
    //   suboptimal (changes production main branch VS preview branches):
    //      VERCEL_ENV
    //      VERCEL_TARGET_ENV
    //
    //   bad (changes per branch):
    //      VERCEL_BRANCH_URL
    //      VERCEL_GIT_COMMIT_REF
    //      VERCEL_GIT_PULL_REQUEST_ID
    //
    //   catastrophic (changes per commit):
    //      NEXT_DEPLOYMENT_ID
    //      VERCEL_URL
    //      VERCEL_DEPLOYMENT_ID
    //      VERCEL_GIT_COMMIT_SHA
    //      VERCEL_GIT_COMMIT_MESSAGE
    //      VERCEL_GIT_COMMIT_AUTHOR_LOGIN
    //      VERCEL_GIT_COMMIT_AUTHOR_NAME
    //      VERCEL_GIT_PREVIOUS_SHA

    let entries = defines
        .0
        .into_iter()
        .map(|(k, value)| (k, FreeVarReference::Value(value)));

    fn wrap_report_next_public_usage(
        public_env_var: &str,
        inner: Option<Box<FreeVarReference>>,
        severity: IssueSeverity,
    ) -> FreeVarReference {
        let message = match public_env_var {
            "NEXT_PUBLIC_NEXT_DEPLOYMENT_ID" | "NEXT_PUBLIC_VERCEL_DEPLOYMENT_ID" => {
                rcstr!(
                    "The deployment id is being inlined.\nThis variable changes frequently, \
                     causing slower deploy times and worse browser client-side caching. Use \
                     `process.env.NEXT_DEPLOYMENT_ID` instead to access the same value without \
                     inlining, for faster deploy times and better browser client-side caching."
                )
            }
            "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA" => {
                rcstr!(
                    "The commit hash is being inlined.\nThis variable changes frequently, causing \
                     slower deploy times and worse browser client-side caching. Consider using \
                     `process.env.NEXT_DEPLOYMENT_ID` to identify a deployment. Alternatively, \
                     use `process.env.VERCEL_GIT_COMMIT_SHA` in server side code and for browser \
                     code, remove it."
                )
            }
            "NEXT_PUBLIC_VERCEL_BRANCH_URL" | "NEXT_PUBLIC_VERCEL_URL" => format!(
                "The deployment url system environment variable is being inlined.\nThis variable \
                 changes frequently, causing slower deploy times and worse browser client-side \
                 caching. For server-side code, replace with `process.env.{}` and for browser \
                 code, read `location.host` instead.",
                public_env_var.strip_prefix("NEXT_PUBLIC_").unwrap(),
            )
            .into(),
            _ => format!(
                "A system environment variable is being inlined.\nThis variable changes \
                 frequently, causing slower deploy times and worse browser client-side caching. \
                 For server-side code, replace with `process.env.{}` and for browser code, try to \
                 remove it.",
                public_env_var.strip_prefix("NEXT_PUBLIC_").unwrap(),
            )
            .into(),
        };
        FreeVarReference::ReportUsage {
            message,
            severity,
            inner,
        }
    }

    let mut list = fxindexset!(
        "NEXT_PUBLIC_NEXT_DEPLOYMENT_ID",
        "NEXT_PUBLIC_VERCEL_BRANCH_URL",
        "NEXT_PUBLIC_VERCEL_DEPLOYMENT_ID",
        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_AUTHOR_LOGIN",
        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_AUTHOR_NAME",
        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE",
        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF",
        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA",
        "NEXT_PUBLIC_VERCEL_GIT_PREVIOUS_SHA",
        "NEXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID",
        "NEXT_PUBLIC_VERCEL_URL",
    );

    let mut entries: FxIndexMap<_, _> = entries
        .map(|(k, value)| {
            let value = if let &[
                DefinableNameSegment::Name(a),
                DefinableNameSegment::Name(b),
                DefinableNameSegment::Name(public_env_var),
            ] = &&*k
                && a == "process"
                && b == "env"
                && list.swap_remove(&**public_env_var)
            {
                wrap_report_next_public_usage(public_env_var, Some(Box::new(value)), severity)
            } else {
                value
            };
            (k, value)
        })
        .collect();

    // For the remaining ones, still add a warning, but without replacement
    for public_env_var in list {
        entries.insert(
            vec![
                rcstr!("process").into(),
                rcstr!("env").into(),
                DefinableNameSegment::Name(public_env_var.into()),
            ],
            wrap_report_next_public_usage(public_env_var, None, severity),
        );
    }

    FreeVarReferences(entries)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, Encode, Decode)]
pub enum PathType {
    PagesPage,
    PagesApi,
    Data,
}

/// Converts a filename within the server root into a next pathname.
#[turbo_tasks::function]
pub async fn pathname_for_path(
    server_root: FileSystemPath,
    server_path: FileSystemPath,
    path_ty: PathType,
) -> Result<Vc<RcStr>> {
    let server_path_value = server_path.clone();
    let path = if let Some(path) = server_root.get_path_to(&server_path_value) {
        path
    } else {
        turbobail!("server_path ({server_path}) is not in server_root ({server_root})");
    };
    let path = match (path_ty, path) {
        // "/" is special-cased to "/index" for data routes.
        (PathType::Data, "") => rcstr!("/index"),
        // `get_path_to` always strips the leading `/` from the path, so we need to add
        // it back here.
        (_, path) => format!("/{path}").into(),
    };

    Ok(Vc::cell(path))
}

// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
// TODO(alexkirsz) There's no need to create an intermediate string here (and
// below), we should instead return an `impl Display`.
pub fn get_asset_prefix_from_pathname(pathname: &str) -> String {
    if pathname == "/" {
        "/index".to_string()
    } else if pathname == "/index" || pathname.starts_with("/index/") {
        format!("/index{pathname}")
    } else {
        pathname.to_string()
    }
}

// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
pub fn get_asset_path_from_pathname(pathname: &str, ext: &str) -> String {
    format!("{}{}", get_asset_prefix_from_pathname(pathname), ext)
}

#[turbo_tasks::function]
pub async fn get_transpiled_packages(
    next_config: Vc<NextConfig>,
    project_path: FileSystemPath,
) -> Result<Vc<Vec<RcStr>>> {
    let mut transpile_packages: Vec<RcStr> = next_config.transpile_packages().owned().await?;

    let default_transpiled_packages: Vec<RcStr> = load_next_js_json_file(
        project_path,
        rcstr!("dist/lib/default-transpiled-packages.json"),
    )
    .await?;

    transpile_packages.extend(default_transpiled_packages.iter().cloned());

    Ok(Vc::cell(transpile_packages))
}

pub async fn foreign_code_context_condition(
    next_config: Vc<NextConfig>,
    project_path: FileSystemPath,
) -> Result<ContextCondition> {
    let transpiled_packages = get_transpiled_packages(next_config, project_path.clone()).await?;

    // The next template files are allowed to import the user's code via import
    // mapping, and imports must use the project-level [ResolveOptions] instead
    // of the `node_modules` specific resolve options (the template files are
    // technically node module files).
    let not_next_template_dir = ContextCondition::not(ContextCondition::InPath(
        get_next_package(project_path.clone())
            .await?
            .join(NEXT_TEMPLATE_PATH)?,
    ));

    let result = ContextCondition::all(vec![
        ContextCondition::InNodeModules,
        not_next_template_dir,
        ContextCondition::not(ContextCondition::any(
            transpiled_packages
                .iter()
                .map(|package| ContextCondition::InDirectory(format!("node_modules/{package}")))
                .collect(),
        )),
    ]);
    Ok(result)
}

/// Determines if the module is an internal asset (i.e overlay, fallback) coming from the embedded
/// FS, don't apply user defined transforms.
//
// TODO: Turbopack specific embed fs paths should be handled by internals of Turbopack itself and
// user config should not try to leak this. However, currently we apply few transform options
// subject to Next.js's configuration even if it's embedded assets.
pub async fn internal_assets_conditions() -> Result<ContextCondition> {
    Ok(ContextCondition::any(vec![
        ContextCondition::InPath(next_js_fs().root().owned().await?),
        ContextCondition::InPath(
            turbopack_ecmascript_runtime::embed_fs()
                .root()
                .owned()
                .await?,
        ),
        ContextCondition::InPath(turbopack_node::embed_js::embed_fs().root().owned().await?),
    ]))
}

pub fn app_function_name(page: impl Display) -> String {
    format!("app{page}")
}
pub fn pages_function_name(page: impl Display) -> String {
    format!("pages{page}")
}

#[derive(
    Default,
    PartialEq,
    Eq,
    Clone,
    Copy,
    Debug,
    TraceRawVcs,
    Deserialize,
    Hash,
    PartialOrd,
    Ord,
    TaskInput,
    NonLocalValue,
    Encode,
    Decode,
)]
#[serde(rename_all = "lowercase")]
pub enum NextRuntime {
    #[default]
    NodeJs,
    #[serde(alias = "experimental-edge")]
    Edge,
}

impl NextRuntime {
    /// Returns conditions that can be used in the Next.js config's turbopack "rules" section for
    /// defining webpack loader configuration.
    pub fn webpack_loader_conditions(&self) -> impl Iterator<Item = WebpackLoaderBuiltinCondition> {
        match self {
            NextRuntime::NodeJs => [WebpackLoaderBuiltinCondition::Node],
            NextRuntime::Edge => [WebpackLoaderBuiltinCondition::EdgeLight],
        }
        .into_iter()
    }

    /// Returns conditions used by `ResolveOptionsContext`.
    pub fn custom_resolve_conditions(&self) -> impl Iterator<Item = RcStr> {
        match self {
            NextRuntime::NodeJs => [rcstr!("node")],
            NextRuntime::Edge => [rcstr!("edge-light")],
        }
        .into_iter()
    }
}

#[derive(PartialEq, Eq, Clone, Debug, TraceRawVcs, NonLocalValue, Encode, Decode)]
pub enum MiddlewareMatcherKind {
    Str(String),
    Matcher(ProxyMatcher),
}

/// Loads a next.js template, replaces `replacements` and `injections` and makes
/// sure there are none left over.
pub async fn load_next_js_template<'b>(
    template_path: &'b str,
    project_path: FileSystemPath,
    replacements: impl IntoIterator<Item = (&'b str, &'b str)>,
    injections: impl IntoIterator<Item = (&'b str, &'b str)>,
    imports: impl IntoIterator<Item = (&'b str, Option<&'b str>)>,
) -> Result<Vc<Box<dyn Source>>> {
    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;

    let content = file_content_rope(template_path.read()).await?;
    let content = content.to_str()?;

    let package_root = get_next_package(project_path).await?;

    let content = expand_next_js_template(
        &content,
        &template_path.path,
        &package_root.path,
        replacements,
        injections,
        imports,
    )?;

    let file = File::from(content);
    let source = VirtualSource::new(
        template_path,
        AssetContent::file(FileContent::Content(file).cell()),
    );

    Ok(Vc::upcast(source))
}

/// Loads a next.js template but does **not** require that any relative imports are present
/// or rewritten. This is intended for small internal templates that do not have their own
/// imports but still use template variables/injections.
pub async fn load_next_js_template_no_imports(
    template_path: &str,
    project_path: FileSystemPath,
    replacements: &[(&str, &str)],
    injections: &[(&str, &str)],
    imports: &[(&str, Option<&str>)],
) -> Result<Vc<Box<dyn Source>>> {
    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;

    let content = file_content_rope(template_path.read()).await?;
    let content = content.to_str()?;

    let package_root = get_next_package(project_path).await?;

    let content = expand_next_js_template_no_imports(
        &content,
        &template_path.path,
        &package_root.path,
        replacements.iter().copied(),
        injections.iter().copied(),
        imports.iter().copied(),
    )?;

    let file = File::from(content);
    let source = VirtualSource::new(
        template_path,
        AssetContent::file(FileContent::Content(file).cell()),
    );

    Ok(Vc::upcast(source))
}

#[turbo_tasks::function]
pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
    let content = &*content.await?;

    let FileContent::Content(file) = content else {
        bail!("Expected file content for file");
    };

    Ok(file.content().to_owned().cell())
}

async fn virtual_next_js_template_path(
    project_path: FileSystemPath,
    file: &str,
) -> Result<FileSystemPath> {
    debug_assert!(!file.contains('/'));
    get_next_package(project_path)
        .await?
        .join(&format!("{NEXT_TEMPLATE_PATH}/{file}"))
}

pub async fn load_next_js_json_file<T: DeserializeOwned>(
    project_path: FileSystemPath,
    sub_path: RcStr,
) -> Result<T> {
    let file_path = get_next_package(project_path.clone())
        .await?
        .join(&sub_path)?;

    let content = &*file_path.read().await?;

    match content.parse_json_ref() {
        FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {e}"),
        FileJsonContent::NotFound => turbobail!("File not found: {file_path:?}",),
        FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
    }
}

pub async fn load_next_js_jsonc_file<T: DeserializeOwned>(
    project_path: FileSystemPath,
    sub_path: RcStr,
) -> Result<T> {
    let file_path = get_next_package(project_path.clone())
        .await?
        .join(&sub_path)?;

    let content = &*file_path.read().await?;

    match content.parse_json_with_comments_ref() {
        FileJsonContent::Unparsable(e) => turbobail!("File is not valid JSON: {e}"),
        FileJsonContent::NotFound => turbobail!("File not found: {file_path}",),
        FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
    }
}

pub fn styles_rule_condition() -> RuleCondition {
    RuleCondition::any(vec![
        RuleCondition::all(vec![
            RuleCondition::ResourcePathEndsWith(".css".into()),
            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.css".into())),
        ]),
        RuleCondition::all(vec![
            RuleCondition::ResourcePathEndsWith(".sass".into()),
            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.sass".into())),
        ]),
        RuleCondition::all(vec![
            RuleCondition::ResourcePathEndsWith(".scss".into()),
            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.scss".into())),
        ]),
        RuleCondition::all(vec![
            RuleCondition::ContentTypeStartsWith("text/css".into()),
            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
                "text/css+module".into(),
            )),
        ]),
        RuleCondition::all(vec![
            RuleCondition::ContentTypeStartsWith("text/sass".into()),
            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
                "text/sass+module".into(),
            )),
        ]),
        RuleCondition::all(vec![
            RuleCondition::ContentTypeStartsWith("text/scss".into()),
            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
                "text/scss+module".into(),
            )),
        ]),
    ])
}
pub fn module_styles_rule_condition() -> RuleCondition {
    RuleCondition::any(vec![
        RuleCondition::ResourcePathEndsWith(".module.css".into()),
        RuleCondition::ResourcePathEndsWith(".module.scss".into()),
        RuleCondition::ResourcePathEndsWith(".module.sass".into()),
        RuleCondition::ContentTypeStartsWith("text/css+module".into()),
        RuleCondition::ContentTypeStartsWith("text/sass+module".into()),
        RuleCondition::ContentTypeStartsWith("text/scss+module".into()),
    ])
}

/// Returns the list of global variables that should be forwarded from the main
/// context to web workers. These are Next.js-specific globals that need to be
/// available in worker contexts.
pub fn worker_forwarded_globals() -> Vec<RcStr> {
    vec![
        rcstr!("NEXT_DEPLOYMENT_ID"),
        rcstr!("NEXT_CLIENT_ASSET_SUFFIX"),
    ]
}
Quest for Codev2.0.0
/
SIGN IN