Skip to main content

next_core/next_root_params/
mod.rs

1use std::iter;
2
3use anyhow::{Result, anyhow, ensure};
4use either::Either;
5use indoc::formatdoc;
6use itertools::Itertools;
7use turbo_rcstr::RcStr;
8use turbo_tasks::{EitherTaskInput, ResolvedVc, Vc};
9use turbo_tasks_fs::{FileContent, FileSystemPath};
10use turbopack_core::{
11    asset::AssetContent,
12    issue::IssueExt,
13    resolve::{
14        ResolveResult,
15        options::{
16            ImportMap, ImportMapResult, ImportMapping, ImportMappingReplacement,
17            ReplacedImportMapping,
18        },
19        parse::Request,
20        pattern::Pattern,
21    },
22    virtual_source::VirtualSource,
23};
24
25use crate::{
26    app_structure::CollectedRootParams, embed_js::next_js_file_path,
27    next_client::ClientContextType, next_server::ServerContextType,
28    next_shared::resolve::InvalidImportModuleIssue,
29};
30
31// The resolver extracts queries from module subpaths.
32const ROOT_PARAM_GETTER_MODULE: &str = "private-next-root-params/getter";
33
34/// The generated getters live in Next's internal filesystem. The collector uses
35/// this directory to distinguish them from user files with the same names.
36pub fn root_param_getters_path() -> Vc<FileSystemPath> {
37    next_js_file_path("root-params".into())
38}
39
40pub async fn insert_next_root_params_mapping(
41    import_map: &mut ImportMap,
42    ty: Either<ServerContextType, ClientContextType>,
43    collected_root_params: Option<Vc<CollectedRootParams>>,
44) -> Result<()> {
45    let mapping = get_next_root_params_mapping(EitherTaskInput(ty), collected_root_params)
46        .to_resolved()
47        .await?;
48    import_map.insert_exact_alias("next/root-params", mapping);
49    import_map.insert_exact_alias(ROOT_PARAM_GETTER_MODULE, mapping);
50    Ok(())
51}
52
53#[turbo_tasks::function]
54async fn get_next_root_params_mapping(
55    ty: EitherTaskInput<ServerContextType, ClientContextType>,
56    collected_root_params: Option<Vc<CollectedRootParams>>,
57) -> Result<Vc<ImportMapping>> {
58    // This mapping goes into the global resolve options, so we want to avoid invalidating it if
59    // value of `collected_root_params` changes (which would invalidate everything else compiled
60    // using those resolve options!).
61    // We can achieve this by using a dynamic import mapping
62    // which only reads `collected_root_params` when producing a mapping result. That way, if
63    // `collected_root_params` changes, the resolve options will remain the same, and
64    // only the mapping result will be invalidated.
65    let mapping = ImportMapping::Dynamic(ResolvedVc::upcast(
66        NextRootParamsMapper::new(ty, collected_root_params)
67            .to_resolved()
68            .await?,
69    ));
70    Ok(mapping.cell())
71}
72
73#[turbo_tasks::value]
74struct NextRootParamsMapper {
75    #[bincode(with = "turbo_bincode::either")]
76    context_type: Either<ServerContextType, ClientContextType>,
77    collected_root_params: Option<ResolvedVc<CollectedRootParams>>,
78}
79
80#[turbo_tasks::value_impl]
81impl NextRootParamsMapper {
82    #[turbo_tasks::function]
83    pub fn new(
84        context_type: EitherTaskInput<ServerContextType, ClientContextType>,
85        collected_root_params: Option<ResolvedVc<CollectedRootParams>>,
86    ) -> Vc<Self> {
87        NextRootParamsMapper {
88            context_type: context_type.0,
89            collected_root_params,
90        }
91        .cell()
92    }
93
94    #[turbo_tasks::function]
95    async fn import_map_result(
96        self: Vc<Self>,
97        param_name: Option<RcStr>,
98    ) -> Result<Vc<ImportMapResult>> {
99        let this = self.await?;
100        Ok(match &this.context_type {
101            Either::Left(server_ty) => match &server_ty {
102                ServerContextType::AppRSC { .. } | ServerContextType::AppRoute { .. } => {
103                    let collected_root_params = *this.collected_root_params.ok_or_else(|| {
104                        anyhow!(
105                            "Invariant: Root params should have been collected for context {:?}. \
106                             This is a bug in Next.js.",
107                            server_ty.clone()
108                        )
109                    })?;
110                    Self::valid_import_map_result(collected_root_params, param_name)
111                }
112                ServerContextType::PagesApi { .. }
113                | ServerContextType::Instrumentation { .. }
114                | ServerContextType::Middleware { .. } => {
115                    // There's no sensible way to use root params outside of the app
116                    // directory. TODO: make sure this error is consistent with webpack
117                    Self::invalid_import_map_result(
118                        "'next/root-params' can only be used inside the App Directory.".into(),
119                    )
120                }
121                _ => {
122                    // In general, the compiler should prevent importing 'next/root-params'
123                    // from client modules, but it doesn't catch everything. If an import
124                    // slips through our validation, make it error.
125                    Self::invalid_import_map_result(
126                        "'next/root-params' cannot be imported from a Client Component module. It \
127                         should only be used from a Server Component."
128                            .into(),
129                    )
130                }
131            },
132            Either::Right(_) => {
133                // In general, the compiler should prevent importing 'next/root-params' from
134                // client modules, but it doesn't catch everything. If an import slips
135                // through our validation, make it error.
136                Self::invalid_import_map_result(
137                    "'next/root-params' cannot be imported from a Client Component module. It \
138                     should only be used from a Server Component."
139                        .into(),
140                )
141            }
142        })
143    }
144
145    #[turbo_tasks::function]
146    async fn valid_import_map_result(
147        collected_root_params: ResolvedVc<CollectedRootParams>,
148        param_name: Option<RcStr>,
149    ) -> Result<Vc<ImportMapResult>> {
150        let collected_root_params = collected_root_params.await?;
151
152        let (path, module_content) = if let Some(param_name) = &param_name {
153            ensure!(
154                collected_root_params.contains(param_name),
155                "Unknown root parameter in generated getter request: {param_name}"
156            );
157            (
158                root_param_getters_path()
159                    .await?
160                    .join(&format!("{param_name}.js"))?,
161                formatdoc!(
162                    r#"
163                        import {{ getRootParam }} from 'next/dist/server/request/root-params';
164                        export function {param_name}() {{
165                            return getRootParam('{param_name}');
166                        }}
167                    "#,
168                ),
169            )
170        } else {
171            // The generated `next/root-params` module only re-exports getters.
172            // The side-effect-free directive lets Turbopack resolve named
173            // imports without including unrelated getter modules.
174            let module_content =
175                iter::once("'use turbopack: no side effects';\nexport {};".to_string())
176                    .chain(collected_root_params.iter().map(|param_name| {
177                        format!(
178                            "export {{ {param_name} }} from \
179                             '{ROOT_PARAM_GETTER_MODULE}?{param_name}';"
180                        )
181                    }))
182                    .join("\n");
183            (
184                next_js_file_path("root-params.js".into()).owned().await?,
185                module_content,
186            )
187        };
188
189        let virtual_source = VirtualSource::new(
190            path,
191            AssetContent::file(FileContent::Content(module_content.into()).cell()),
192        )
193        .to_resolved()
194        .await?;
195
196        let import_map_result = ImportMapResult::Result(
197            ResolveResult::source(ResolvedVc::upcast(virtual_source)).resolved_cell(),
198        );
199        Ok(import_map_result.cell())
200    }
201
202    #[turbo_tasks::function]
203    async fn invalid_import_map_result(message: RcStr) -> Result<Vc<ImportMapResult>> {
204        let path: FileSystemPath = next_js_file_path("root-params.js".into()).owned().await?;
205
206        // error the compilation.
207        InvalidImportModuleIssue {
208            file_path: path.clone(),
209            messages: vec![message.clone()],
210            skip_context_message: false,
211        }
212        .resolved_cell()
213        .emit();
214
215        // map to a dummy module that rethrows the error at runtime.
216        let virtual_source = VirtualSource::new(
217            path.clone(),
218            AssetContent::file(
219                FileContent::Content(
220                    format!("throw new Error({})", serde_json::to_string(&message)?).into(),
221                )
222                .cell(),
223            ),
224        )
225        .to_resolved()
226        .await?;
227
228        let import_map_result = ImportMapResult::Result(
229            ResolveResult::source(ResolvedVc::upcast(virtual_source)).resolved_cell(),
230        );
231        Ok(import_map_result.cell())
232    }
233}
234
235#[turbo_tasks::value_impl]
236impl ImportMappingReplacement for NextRootParamsMapper {
237    #[turbo_tasks::function]
238    fn replace(&self, _capture: Vc<Pattern>) -> Vc<ReplacedImportMapping> {
239        ReplacedImportMapping::Ignore.cell()
240    }
241
242    #[turbo_tasks::function]
243    async fn result(
244        self: Vc<Self>,
245        _lookup_path: FileSystemPath,
246        request: Vc<Request>,
247    ) -> Result<Vc<ImportMapResult>> {
248        let request = request.await?;
249        let param_name = match &*request {
250            Request::Module { query, .. }
251                if request.request().as_deref() == Some(ROOT_PARAM_GETTER_MODULE) =>
252            {
253                Some(
254                    query
255                        .strip_prefix('?')
256                        .ok_or_else(|| {
257                            anyhow!("Missing root parameter in generated getter request")
258                        })?
259                        .into(),
260                )
261            }
262            _ => None,
263        };
264        // Share generated sources across importers with the same root name.
265        Ok(self.import_map_result(param_name))
266    }
267}