Skip to main content

next_core/next_pages/
page_entry.rs

1use std::io::Write;
2
3use anyhow::{Result, bail};
4use serde::Serialize;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{ResolvedVc, Vc, fxindexmap};
7use turbo_tasks_fs::{File, FileContent, FileSystemPath, rope::RopeBuilder};
8use turbopack_core::{
9    asset::{Asset, AssetContent},
10    context::AssetContext,
11    file_source::FileSource,
12    module::Module,
13    reference_type::{EntryReferenceSubType, ReferenceType},
14    source::Source,
15    virtual_source::VirtualSource,
16};
17
18use crate::{
19    next_config::NextConfig,
20    next_edge::entry::wrap_edge_entry,
21    pages_structure::{PagesStructure, PagesStructureItem},
22    util::{NextRuntime, file_content_rope, load_next_js_template, pages_function_name},
23};
24
25#[turbo_tasks::value]
26pub struct PageSsrEntryModule {
27    pub ssr_module: ResolvedVc<Box<dyn Module>>,
28    pub app_module: Option<ResolvedVc<Box<dyn Module>>>,
29    pub document_module: Option<ResolvedVc<Box<dyn Module>>>,
30}
31
32#[turbo_tasks::function]
33pub async fn create_page_ssr_entry_module(
34    pathname: RcStr,
35    reference_type: ReferenceType,
36    project_root: FileSystemPath,
37    ssr_module_context: Vc<Box<dyn AssetContext>>,
38    source: Vc<Box<dyn Source>>,
39    next_original_name: RcStr,
40    pages_structure: Vc<PagesStructure>,
41    next_config: Vc<NextConfig>,
42    runtime: NextRuntime,
43) -> Result<Vc<PageSsrEntryModule>> {
44    let definition_page = next_original_name;
45    let definition_pathname = pathname;
46
47    let ssr_module = ssr_module_context
48        .process(source, reference_type.clone())
49        .module()
50        .to_resolved()
51        .await?;
52
53    let template_file = match &reference_type {
54        ReferenceType::Entry(EntryReferenceSubType::Page)
55        | ReferenceType::Entry(EntryReferenceSubType::PageData) => {
56            // Load the Page entry file.
57            match runtime {
58                NextRuntime::NodeJs => "pages.js",
59                NextRuntime::Edge => "edge-ssr.js",
60            }
61        }
62        ReferenceType::Entry(EntryReferenceSubType::PagesApi) => {
63            // Load the Pages API entry file.
64            match runtime {
65                NextRuntime::NodeJs => "pages-api.js",
66                NextRuntime::Edge => "pages-edge-api.js",
67            }
68        }
69        _ => bail!("Invalid path type"),
70    };
71
72    let inner = rcstr!("INNER_PAGE");
73    let inner_document = rcstr!("INNER_DOCUMENT");
74    let inner_app = rcstr!("INNER_APP");
75    let inner_error = rcstr!("INNER_ERROR");
76    let inner_error_500 = rcstr!("INNER_500");
77
78    let mut replacements = vec![
79        ("VAR_DEFINITION_PATHNAME", &*definition_pathname),
80        ("VAR_USERLAND", &*inner),
81    ];
82
83    let is_page = matches!(
84        reference_type,
85        ReferenceType::Entry(EntryReferenceSubType::Page)
86            | ReferenceType::Entry(EntryReferenceSubType::PageData)
87    );
88    if !(is_page && runtime == NextRuntime::Edge) {
89        replacements.push(("VAR_DEFINITION_PAGE", &*definition_page));
90    }
91    if is_page {
92        replacements.push(("VAR_MODULE_DOCUMENT", &*inner_document));
93        replacements.push(("VAR_MODULE_APP", &*inner_app));
94        if is_page && runtime == NextRuntime::Edge {
95            replacements.push(("VAR_MODULE_GLOBAL_ERROR", &*inner_error));
96        }
97    }
98
99    let pages_structure_ref = pages_structure.await?;
100    let mut cache_handler_inner_assets = fxindexmap! {};
101    let mut cache_handler_imports = String::new();
102    let mut cache_handler_registration = String::new();
103    let mut incremental_cache_handler_import = None;
104
105    if runtime == NextRuntime::Edge {
106        if is_page {
107            let cache_handlers = next_config.cache_handlers_map().owned().await?;
108            for (index, (kind, handler_path)) in cache_handlers.iter().enumerate() {
109                let cache_handler_inner: RcStr = format!("INNER_CACHE_HANDLER_{index}").into();
110                let cache_handler_var = format!("cacheHandler{index}");
111                cache_handler_imports.push_str(&format!(
112                    "import {cache_handler_var} from {};\n",
113                    serde_json::to_string(&*cache_handler_inner)?
114                ));
115                cache_handler_registration.push_str(&format!(
116                    "  cacheHandlers.setCacheHandler({}, {cache_handler_var});\n",
117                    serde_json::to_string(kind.as_str())?
118                ));
119
120                let cache_handler_module = ssr_module_context
121                    .process(
122                        Vc::upcast(FileSource::new(project_root.join(handler_path)?)),
123                        ReferenceType::Undefined,
124                    )
125                    .module()
126                    .to_resolved()
127                    .await?;
128                cache_handler_inner_assets.insert(cache_handler_inner, cache_handler_module);
129            }
130        }
131
132        for cache_handler_path in next_config
133            .cache_handler(project_root.clone())
134            .await?
135            .into_iter()
136        {
137            let cache_handler_inner = rcstr!("INNER_INCREMENTAL_CACHE_HANDLER");
138            incremental_cache_handler_import = Some(cache_handler_inner.clone());
139            let cache_handler_module = ssr_module_context
140                .process(
141                    Vc::upcast(FileSource::new(cache_handler_path.clone())),
142                    ReferenceType::Undefined,
143                )
144                .module()
145                .to_resolved()
146                .await?;
147            cache_handler_inner_assets.insert(cache_handler_inner, cache_handler_module);
148        }
149    }
150
151    let (injections, imports) = if is_page && runtime == NextRuntime::Edge {
152        let injections = vec![
153            (
154                "pageRouteModuleOptions",
155                serde_json::to_string(&get_route_module_options(
156                    definition_page.clone(),
157                    definition_pathname.clone(),
158                ))?,
159            ),
160            (
161                "errorRouteModuleOptions",
162                serde_json::to_string(&get_route_module_options(
163                    rcstr!("/_error"),
164                    rcstr!("/_error"),
165                ))?,
166            ),
167            (
168                "user500RouteModuleOptions",
169                serde_json::to_string(&get_route_module_options(rcstr!("/500"), rcstr!("/500")))?,
170            ),
171            ("cacheHandlerImports", cache_handler_imports),
172            ("cacheHandlerRegistration", cache_handler_registration),
173        ];
174        let imports = vec![
175            ("incrementalCacheHandler", incremental_cache_handler_import),
176            (
177                "userland500Page",
178                pages_structure_ref
179                    .error_500
180                    .map(|_| inner_error_500.clone()),
181            ),
182        ];
183        (injections, imports)
184    } else if runtime == NextRuntime::Edge {
185        (
186            vec![],
187            vec![("incrementalCacheHandler", incremental_cache_handler_import)],
188        )
189    } else {
190        (vec![], vec![])
191    };
192
193    // Load the file from the next.js codebase.
194    let mut source = load_next_js_template(
195        template_file,
196        project_root.clone(),
197        replacements,
198        injections.iter().map(|(k, v)| (*k, &**v)),
199        imports
200            .iter()
201            .map(|(k, v)| (*k, v.as_ref().map(|value| value.as_str()))),
202    )
203    .await?;
204
205    // When we're building the instrumentation page (only when the
206    // instrumentation file conflicts with a page also labeled
207    // /instrumentation) hoist the `register` method.
208    if is_page
209        && (definition_page == "/instrumentation" || definition_page == "/src/instrumentation")
210    {
211        let file = &*file_content_rope(source.content().file_content()).await?;
212
213        let mut result = RopeBuilder::default();
214        result += file;
215
216        writeln!(
217            result,
218            r#"export const register = hoist(userland, "register")"#
219        )?;
220
221        let file = File::from(result.build());
222
223        source = Vc::upcast(VirtualSource::new_with_ident(
224            source.ident(),
225            AssetContent::file(FileContent::Content(file).cell()),
226        ));
227    }
228
229    let mut inner_assets = fxindexmap! {
230        inner => ssr_module,
231    };
232    inner_assets.extend(cache_handler_inner_assets);
233
234    // for PagesData we apply a ?server-data query parameter to avoid conflicts with the Page
235    // module.
236    // We need to copy that to all the modules we create.
237    let source_query = source.ident().await?.query.clone();
238
239    let (app_module, document_module) = if is_page {
240        // We process the document and app modules in the same context and reference type.
241        let document_module = process_global_item(
242            *pages_structure_ref.document,
243            reference_type.clone(),
244            source_query.clone(),
245            ssr_module_context,
246        )
247        .to_resolved()
248        .await?;
249        let app_module = process_global_item(
250            *pages_structure_ref.app,
251            reference_type.clone(),
252            source_query.clone(),
253            ssr_module_context,
254        )
255        .to_resolved()
256        .await?;
257        inner_assets.insert(inner_document, document_module);
258        inner_assets.insert(inner_app, app_module);
259
260        if is_page && runtime == NextRuntime::Edge {
261            inner_assets.insert(
262                inner_error,
263                process_global_item(
264                    *pages_structure_ref.error,
265                    reference_type.clone(),
266                    source_query.clone(),
267                    ssr_module_context,
268                )
269                .to_resolved()
270                .await?,
271            );
272
273            if let Some(error_500) = pages_structure_ref.error_500 {
274                inner_assets.insert(
275                    inner_error_500,
276                    process_global_item(
277                        *error_500,
278                        reference_type.clone(),
279                        source_query.clone(),
280                        ssr_module_context,
281                    )
282                    .to_resolved()
283                    .await?,
284                );
285            }
286        }
287        (Some(app_module), Some(document_module))
288    } else {
289        (None, None)
290    };
291
292    let mut ssr_module = ssr_module_context
293        .process(
294            source,
295            ReferenceType::Internal(ResolvedVc::cell(inner_assets)),
296        )
297        .module();
298
299    if matches!(runtime, NextRuntime::Edge) {
300        ssr_module = wrap_edge_entry(
301            ssr_module_context,
302            project_root,
303            ssr_module,
304            pages_function_name(&definition_page).into(),
305        );
306    }
307
308    Ok(PageSsrEntryModule {
309        ssr_module: ssr_module.to_resolved().await?,
310        app_module,
311        document_module,
312    }
313    .cell())
314}
315
316#[turbo_tasks::function]
317async fn process_global_item(
318    item: Vc<PagesStructureItem>,
319    reference_type: ReferenceType,
320    source_query: RcStr,
321    module_context: Vc<Box<dyn AssetContext>>,
322) -> Result<Vc<Box<dyn Module>>> {
323    let source = Vc::upcast(FileSource::new_with_query(
324        item.file_path().owned().await?,
325        source_query,
326    ));
327    Ok(module_context.process(source, reference_type).module())
328}
329
330#[derive(Serialize)]
331struct PartialRouteModuleOptions {
332    definition: RouteDefinition,
333}
334
335#[derive(Serialize)]
336struct RouteDefinition {
337    kind: RcStr,
338    bundle_path: RcStr,
339    filename: RcStr,
340    /// Describes the pathname including all internal modifiers such as
341    /// intercepting routes, parallel routes and route/page suffixes that are
342    /// not part of the pathname.
343    page: RcStr,
344
345    /// The pathname (including dynamic placeholders) for a route to resolve.
346    pathname: RcStr,
347}
348
349fn get_route_module_options(page: RcStr, pathname: RcStr) -> PartialRouteModuleOptions {
350    PartialRouteModuleOptions {
351        definition: RouteDefinition {
352            kind: rcstr!("PAGES"),
353            page,
354            pathname,
355            // The following aren't used in production.
356            bundle_path: rcstr!(""),
357            filename: rcstr!(""),
358        },
359    }
360}