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, 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    runtime: NextRuntime,
42    next_config: Vc<NextConfig>,
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, runtime) {
54        (ReferenceType::Entry(EntryReferenceSubType::Page), _)
55        | (ReferenceType::Entry(EntryReferenceSubType::PageData), _) => {
56            // Load the Page entry file.
57            "pages.js"
58        }
59        (ReferenceType::Entry(EntryReferenceSubType::PagesApi), NextRuntime::NodeJs) => {
60            // Load the Pages API entry file.
61            "pages-api.js"
62        }
63        (ReferenceType::Entry(EntryReferenceSubType::PagesApi), NextRuntime::Edge) => {
64            // Load the Pages API entry file.
65            "pages-edge-api.js"
66        }
67        _ => bail!("Invalid path type"),
68    };
69
70    let inner = rcstr!("INNER_PAGE");
71
72    let inner_document = rcstr!("INNER_DOCUMENT");
73    let inner_app = rcstr!("INNER_APP");
74
75    let mut replacements = vec![
76        ("VAR_DEFINITION_PAGE", &*definition_page),
77        ("VAR_DEFINITION_PATHNAME", &definition_pathname),
78        ("VAR_USERLAND", &inner),
79    ];
80
81    let is_page = matches!(
82        reference_type,
83        ReferenceType::Entry(EntryReferenceSubType::Page)
84            | ReferenceType::Entry(EntryReferenceSubType::PageData)
85    );
86    if is_page {
87        replacements.push(("VAR_MODULE_DOCUMENT", &inner_document));
88        replacements.push(("VAR_MODULE_APP", &inner_app));
89    }
90    let source_ident = source.ident().await?;
91    // for PagesData we apply a ?server-data query parameter to avoid conflicts with the Page
92    // module.
93    // We need to copy that to all the modules we create.
94    let source_query = source_ident.query.clone();
95
96    // Load the file from the next.js codebase.
97    let mut source =
98        load_next_js_template(template_file, project_root.clone(), &replacements, &[], &[]).await?;
99
100    // When we're building the instrumentation page (only when the
101    // instrumentation file conflicts with a page also labeled
102    // /instrumentation) hoist the `register` method.
103    if is_page
104        && (definition_page == "/instrumentation" || definition_page == "/src/instrumentation")
105    {
106        let file = &*file_content_rope(source.content().file_content()).await?;
107
108        let mut result = RopeBuilder::default();
109        result += file;
110
111        writeln!(
112            result,
113            r#"export const register = hoist(userland, "register")"#
114        )?;
115
116        let file = File::from(result.build());
117
118        source = Vc::upcast(VirtualSource::new_with_ident(
119            source.ident(),
120            AssetContent::file(file.into()),
121        ));
122    }
123
124    let mut inner_assets = fxindexmap! {
125        inner => ssr_module,
126    };
127
128    let pages_structure_ref = pages_structure.await?;
129
130    let (app_module, document_module) = if is_page {
131        // We process the document and app modules in the same context and reference type.
132        let document_module = process_global_item(
133            *pages_structure_ref.document,
134            reference_type.clone(),
135            source_query.clone(),
136            ssr_module_context,
137        )
138        .to_resolved()
139        .await?;
140        let app_module = process_global_item(
141            *pages_structure_ref.app,
142            reference_type.clone(),
143            source_query.clone(),
144            ssr_module_context,
145        )
146        .to_resolved()
147        .await?;
148        inner_assets.insert(inner_document, document_module);
149        inner_assets.insert(inner_app, app_module);
150        (Some(app_module), Some(document_module))
151    } else {
152        (None, None)
153    };
154
155    let mut ssr_module = ssr_module_context
156        .process(
157            source,
158            ReferenceType::Internal(ResolvedVc::cell(inner_assets)),
159        )
160        .module();
161
162    if matches!(runtime, NextRuntime::Edge) {
163        if is_page {
164            ssr_module = wrap_edge_page(
165                ssr_module_context,
166                project_root,
167                ssr_module,
168                definition_page.clone(),
169                definition_pathname.clone(),
170                reference_type,
171                pages_structure,
172                next_config,
173                source_query.clone(),
174            );
175        } else {
176            ssr_module = wrap_edge_entry(
177                ssr_module_context,
178                project_root,
179                ssr_module,
180                pages_function_name(&definition_page).into(),
181            );
182        }
183    }
184
185    Ok(PageSsrEntryModule {
186        ssr_module: ssr_module.to_resolved().await?,
187        app_module,
188        document_module,
189    }
190    .cell())
191}
192
193#[turbo_tasks::function]
194async fn process_global_item(
195    item: Vc<PagesStructureItem>,
196    reference_type: ReferenceType,
197    source_query: RcStr,
198    module_context: Vc<Box<dyn AssetContext>>,
199) -> Result<Vc<Box<dyn Module>>> {
200    let source = Vc::upcast(FileSource::new_with_query(
201        item.file_path().owned().await?,
202        source_query,
203    ));
204    Ok(module_context.process(source, reference_type).module())
205}
206
207#[turbo_tasks::function]
208async fn wrap_edge_page(
209    asset_context: Vc<Box<dyn AssetContext>>,
210    project_root: FileSystemPath,
211    entry: ResolvedVc<Box<dyn Module>>,
212    page: RcStr,
213    pathname: RcStr,
214    reference_type: ReferenceType,
215    pages_structure: Vc<PagesStructure>,
216    next_config: Vc<NextConfig>,
217    source_query: RcStr,
218) -> Result<Vc<Box<dyn Module>>> {
219    const INNER: &str = "INNER_PAGE_ENTRY";
220
221    const INNER_DOCUMENT: &str = "INNER_DOCUMENT";
222    const INNER_APP: &str = "INNER_APP";
223    const INNER_ERROR: &str = "INNER_ERROR";
224    const INNER_ERROR_500: &str = "INNER_500";
225
226    let next_config_val = &*next_config.await?;
227
228    let source = load_next_js_template(
229        "edge-ssr.js",
230        project_root.clone(),
231        &[
232            ("VAR_USERLAND", INNER),
233            ("VAR_PAGE", &pathname),
234            ("VAR_MODULE_DOCUMENT", INNER_DOCUMENT),
235            ("VAR_MODULE_APP", INNER_APP),
236            ("VAR_MODULE_GLOBAL_ERROR", INNER_ERROR),
237        ],
238        &[
239            // TODO do we really need to pass the entire next config here?
240            // This is bad for invalidation as any config change will invalidate this
241            ("nextConfig", &*serde_json::to_string(next_config_val)?),
242            (
243                "pageRouteModuleOptions",
244                &serde_json::to_string(&get_route_module_options(page.clone(), pathname.clone()))?,
245            ),
246            (
247                "errorRouteModuleOptions",
248                &serde_json::to_string(&get_route_module_options(
249                    rcstr!("/_error"),
250                    rcstr!("/_error"),
251                ))?,
252            ),
253            (
254                "user500RouteModuleOptions",
255                &serde_json::to_string(&get_route_module_options(rcstr!("/500"), rcstr!("/500")))?,
256            ),
257        ],
258        &[
259            // TODO
260            ("incrementalCacheHandler", None),
261            (
262                "userland500Page",
263                pages_structure.await?.error_500.map(|_| INNER_ERROR_500),
264            ),
265        ],
266    )
267    .await?;
268
269    let pages_structure_ref = pages_structure.await?;
270
271    let mut inner_assets = fxindexmap! {
272        INNER.into() => entry,
273        INNER_DOCUMENT.into() => process_global_item(
274            *pages_structure_ref.document,
275            reference_type.clone(),
276            source_query.clone(),
277            asset_context,
278        ).to_resolved().await?,
279        INNER_APP.into() => process_global_item(
280            *pages_structure_ref.app,
281            reference_type.clone(),
282            source_query.clone(),
283            asset_context,
284        ).to_resolved().await?,
285        INNER_ERROR.into() => process_global_item(
286            *pages_structure_ref.error,
287            reference_type.clone(),
288            source_query.clone(),
289            asset_context,
290        ).to_resolved().await?,
291    };
292
293    if let Some(error_500) = pages_structure_ref.error_500 {
294        inner_assets.insert(
295            INNER_ERROR_500.into(),
296            process_global_item(
297                *error_500,
298                reference_type.clone(),
299                source_query.clone(),
300                asset_context,
301            )
302            .to_resolved()
303            .await?,
304        );
305    }
306
307    let wrapped = asset_context
308        .process(
309            source,
310            ReferenceType::Internal(ResolvedVc::cell(inner_assets)),
311        )
312        .module();
313
314    Ok(wrap_edge_entry(
315        asset_context,
316        project_root,
317        wrapped,
318        pages_function_name(&page).into(),
319    ))
320}
321
322#[derive(Serialize)]
323struct PartialRouteModuleOptions {
324    definition: RouteDefinition,
325}
326
327#[derive(Serialize)]
328struct RouteDefinition {
329    kind: RcStr,
330    bundle_path: RcStr,
331    filename: RcStr,
332    /// Describes the pathname including all internal modifiers such as
333    /// intercepting routes, parallel routes and route/page suffixes that are
334    /// not part of the pathname.
335    page: RcStr,
336
337    /// The pathname (including dynamic placeholders) for a route to resolve.
338    pathname: RcStr,
339}
340
341fn get_route_module_options(page: RcStr, pathname: RcStr) -> PartialRouteModuleOptions {
342    PartialRouteModuleOptions {
343        definition: RouteDefinition {
344            kind: rcstr!("PAGES"),
345            page,
346            pathname,
347            // The following aren't used in production.
348            bundle_path: rcstr!(""),
349            filename: rcstr!(""),
350        },
351    }
352}