next.js/crates/next-core/src/next_shared/resolve.rs
resolve.rs328 lines10.0 KB
use anyhow::Result;
use async_trait::async_trait;
use phf::phf_map;
use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::{ResolvedVc, Vc};
use turbo_tasks_fs::{
    FileSystemPath,
    glob::{Glob, GlobOptions},
};
use turbopack_core::{
    diagnostics::DiagnosticExt,
    file_source::FileSource,
    issue::{Issue, IssueSeverity, IssueStage, StyledString},
    reference_type::ReferenceType,
    resolve::{
        ExternalTraced, ExternalType, ResolveResult, ResolveResultItem, ResolveResultOption,
        parse::Request,
        pattern::Pattern,
        plugin::{
            AfterResolvePlugin, AfterResolvePluginCondition, BeforeResolvePlugin,
            BeforeResolvePluginCondition,
        },
    },
};

use crate::{next_server::ServerContextType, next_telemetry::ModuleFeatureTelemetry};

// Set of the features we want to track, following existing references in
// webpack/plugins/telemetry-plugin.
static FEATURE_MODULES: phf::Map<&'static str, &'static [&'static str]> = phf_map! {
    "next" => &[
        "/image",
        "/future/image",
        "/legacy/image",
        "/script",
        "/dynamic",
        "/font/google",
        "/font/local",
    ],
    "@next/font" => &["/google", "/local"],
};

#[turbo_tasks::value(shared)]
pub struct InvalidImportModuleIssue {
    // TODO(PACK-4879): The filepath is incorrect and there should be a fine grained source
    // location pointing at the import/require
    pub file_path: FileSystemPath,
    pub messages: Vec<RcStr>,
    pub skip_context_message: bool,
}

#[async_trait]
#[turbo_tasks::value_impl]
impl Issue for InvalidImportModuleIssue {
    fn severity(&self) -> IssueSeverity {
        IssueSeverity::Error
    }

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

    async fn title(&self) -> Result<StyledString> {
        Ok(StyledString::Text(rcstr!("Invalid import")))
    }

    async fn file_path(&self) -> Result<FileSystemPath> {
        Ok(self.file_path.clone())
    }

    async fn description(&self) -> Result<Option<StyledString>> {
        let mut messages = self.messages.clone();
        if !self.skip_context_message {
            //[TODO]: how do we get the import trace?
            messages.push(
                format!(
                    "The error was caused by importing '{}'",
                    self.file_path.path
                )
                .into(),
            );
        }

        Ok(Some(StyledString::Line(
            messages
                .iter()
                .map(|v| StyledString::Text(format!("{v}\n").into()))
                .collect::<Vec<StyledString>>(),
        )))
    }
}

#[turbo_tasks::value]
pub(crate) struct NextExternalResolvePlugin {
    project_path: FileSystemPath,
}

#[turbo_tasks::value_impl]
impl NextExternalResolvePlugin {
    #[turbo_tasks::function]
    pub fn new(project_path: FileSystemPath) -> Vc<Self> {
        NextExternalResolvePlugin { project_path }.cell()
    }
}

#[turbo_tasks::value_impl]
impl AfterResolvePlugin for NextExternalResolvePlugin {
    #[turbo_tasks::function]
    async fn after_resolve_condition(&self) -> Result<Vc<AfterResolvePluginCondition>> {
        Ok(AfterResolvePluginCondition::new_with_glob(
            self.project_path.root().owned().await?,
            Glob::new(
                rcstr!("**/next/dist/**/*.{external,runtime.dev,runtime.prod}.js"),
                GlobOptions::default(),
            ),
        ))
    }

    #[turbo_tasks::function]
    async fn after_resolve(
        &self,
        fs_path: FileSystemPath,
        _lookup_path: FileSystemPath,
        _reference_type: ReferenceType,
        _request: Vc<Request>,
    ) -> Result<Vc<ResolveResultOption>> {
        let path = fs_path.path.to_string();
        // Find the starting index of 'next/dist' and slice from that point. It should
        // always be found since the glob pattern above is specific enough.
        let starting_index = path.find("next/dist").unwrap();
        let specifier = &path[starting_index..];
        // Replace '/esm/' with '/' to match the CJS version of the file.
        let specifier: RcStr = specifier.replace("/esm/", "/").into();

        Ok(Vc::cell(Some(
            ResolveResult::primary(ResolveResultItem::External {
                name: specifier.clone(),
                ty: ExternalType::CommonJs,
                traced: ExternalTraced::Traced,
                target: None,
            })
            .resolved_cell(),
        )))
    }
}

#[turbo_tasks::value]
pub(crate) struct NextNodeSharedRuntimeResolvePlugin {
    root: FileSystemPath,
    server_context_type: ServerContextType,
}

#[turbo_tasks::value_impl]
impl NextNodeSharedRuntimeResolvePlugin {
    #[turbo_tasks::function]
    pub fn new(root: FileSystemPath, server_context_type: ServerContextType) -> Vc<Self> {
        NextNodeSharedRuntimeResolvePlugin {
            root,
            server_context_type,
        }
        .cell()
    }
}

#[turbo_tasks::value_impl]
impl AfterResolvePlugin for NextNodeSharedRuntimeResolvePlugin {
    #[turbo_tasks::function]
    async fn after_resolve_condition(&self) -> Result<Vc<AfterResolvePluginCondition>> {
        Ok(AfterResolvePluginCondition::new_with_glob(
            self.root.root().owned().await?,
            Glob::new(
                rcstr!("**/next/dist/**/*.shared-runtime.js"),
                GlobOptions::default(),
            ),
        ))
    }

    #[turbo_tasks::function]
    async fn after_resolve(
        &self,
        fs_path: FileSystemPath,
        _lookup_path: FileSystemPath,
        _reference_type: ReferenceType,
        _request: Vc<Request>,
    ) -> Result<Vc<ResolveResultOption>> {
        let stem = fs_path.file_stem();
        let stem = stem.unwrap_or_default();
        let stem = stem.replace(".shared-runtime", "");

        let resource_request = format!(
            "next/dist/server/route-modules/{}/vendored/contexts/{}.js",
            match self.server_context_type {
                ServerContextType::AppRoute { .. } => "app-route",
                ServerContextType::AppSSR { .. } | ServerContextType::AppRSC { .. } => "app-page",
                // Use default pages context for all other contexts.
                _ => "pages",
            },
            stem
        );

        let raw_fs_path = fs_path.clone();
        let path = raw_fs_path.path.to_string();

        // Find the starting index of 'next/dist' and slice from that point. It should
        // always be found since the glob pattern above is specific enough.
        let starting_index = path.find("next/dist").unwrap();

        let (base, _) = path.split_at(starting_index);

        let new_path = fs_path
            .root()
            .await?
            .join(&format!("{base}/{resource_request}"))?;

        Ok(Vc::cell(Some(
            ResolveResult::source(ResolvedVc::upcast(
                FileSource::new(new_path).to_resolved().await?,
            ))
            .resolved_cell(),
        )))
    }
}

/// A resolver plugin tracks the usage of certain import paths, emit
/// telemetry events if there is a match.
#[turbo_tasks::value]
pub(crate) struct ModuleFeatureReportResolvePlugin {
    root: FileSystemPath,
}

#[turbo_tasks::value_impl]
impl ModuleFeatureReportResolvePlugin {
    #[turbo_tasks::function]
    pub fn new(root: FileSystemPath) -> Vc<Self> {
        ModuleFeatureReportResolvePlugin { root }.cell()
    }
}

#[turbo_tasks::value_impl]
impl BeforeResolvePlugin for ModuleFeatureReportResolvePlugin {
    #[turbo_tasks::function]
    fn before_resolve_condition(&self) -> Vc<BeforeResolvePluginCondition> {
        BeforeResolvePluginCondition::from_modules(Vc::cell(
            FEATURE_MODULES
                .keys()
                .map(|k| (*k).into())
                .collect::<Vec<RcStr>>(),
        ))
    }

    #[turbo_tasks::function]
    async fn before_resolve(
        &self,
        _lookup_path: FileSystemPath,
        _reference_type: ReferenceType,
        request: Vc<Request>,
    ) -> Result<Vc<ResolveResultOption>> {
        if let Request::Module {
            module: Pattern::Constant(module),
            path,
            query: _,
            fragment: _,
        } = &*request.await?
        {
            let feature_module = FEATURE_MODULES.get(module.as_str());
            if let Some(feature_module) = feature_module {
                let sub_path = feature_module
                    .iter()
                    .find(|sub_path| path.is_match(sub_path));

                if let Some(sub_path) = sub_path {
                    ModuleFeatureTelemetry::new(format!("{module}{sub_path}").into(), 1)
                        .resolved_cell()
                        .emit();
                }
            }
        }

        Ok(ResolveResultOption::none())
    }
}

#[turbo_tasks::value]
pub(crate) struct NextSharedRuntimeResolvePlugin {
    root: FileSystemPath,
}

#[turbo_tasks::value_impl]
impl NextSharedRuntimeResolvePlugin {
    #[turbo_tasks::function]
    pub fn new(root: FileSystemPath) -> Vc<Self> {
        NextSharedRuntimeResolvePlugin { root }.cell()
    }
}

#[turbo_tasks::value_impl]
impl AfterResolvePlugin for NextSharedRuntimeResolvePlugin {
    #[turbo_tasks::function]
    async fn after_resolve_condition(&self) -> Result<Vc<AfterResolvePluginCondition>> {
        Ok(AfterResolvePluginCondition::new_with_glob(
            self.root.root().owned().await?,
            Glob::new(
                rcstr!("**/next/dist/esm/**/*.shared-runtime.js"),
                GlobOptions::default(),
            ),
        ))
    }

    #[turbo_tasks::function]
    async fn after_resolve(
        &self,
        fs_path: FileSystemPath,
        _lookup_path: FileSystemPath,
        _reference_type: ReferenceType,
        _request: Vc<Request>,
    ) -> Result<Vc<ResolveResultOption>> {
        let raw_fs_path = fs_path.clone();
        let modified_path = raw_fs_path.path.replace("next/dist/esm/", "next/dist/");
        let new_path = fs_path.root().await?.join(&modified_path)?;
        Ok(Vc::cell(Some(
            ResolveResult::source(ResolvedVc::upcast(
                FileSource::new(new_path).to_resolved().await?,
            ))
            .resolved_cell(),
        )))
    }
}
Quest for Codev2.0.0
/
SIGN IN