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_edge::entry::wrap_edge_entry,
20    pages_structure::{PagesStructure, PagesStructureItem},
21    util::{NextRuntime, file_content_rope, load_next_js_template, pages_function_name},
22};
23
24#[turbo_tasks::value]
25pub struct PageSsrEntryModule {
26    pub ssr_module: ResolvedVc<Box<dyn Module>>,
27    pub app_module: Option<ResolvedVc<Box<dyn Module>>>,
28    pub document_module: Option<ResolvedVc<Box<dyn Module>>>,
29}
30
31#[turbo_tasks::function]
32pub async fn create_page_ssr_entry_module(
33    pathname: RcStr,
34    reference_type: ReferenceType,
35    project_root: FileSystemPath,
36    ssr_module_context: Vc<Box<dyn AssetContext>>,
37    source: Vc<Box<dyn Source>>,
38    next_original_name: RcStr,
39    pages_structure: Vc<PagesStructure>,
40    runtime: NextRuntime,
41) -> Result<Vc<PageSsrEntryModule>> {
42    let definition_page = next_original_name;
43    let definition_pathname = pathname;
44
45    let ssr_module = ssr_module_context
46        .process(source, reference_type.clone())
47        .module()
48        .to_resolved()
49        .await?;
50
51    let template_file = match &reference_type {
52        ReferenceType::Entry(EntryReferenceSubType::Page)
53        | ReferenceType::Entry(EntryReferenceSubType::PageData) => {
54            // Load the Page entry file.
55            match runtime {
56                NextRuntime::NodeJs => "pages.js",
57                NextRuntime::Edge => "edge-ssr.js",
58            }
59        }
60        ReferenceType::Entry(EntryReferenceSubType::PagesApi) => {
61            // Load the Pages API entry file.
62            match runtime {
63                NextRuntime::NodeJs => "pages-api.js",
64                NextRuntime::Edge => "pages-edge-api.js",
65            }
66        }
67        _ => bail!("Invalid path type"),
68    };
69
70    let inner = rcstr!("INNER_PAGE");
71    let inner_document = rcstr!("INNER_DOCUMENT");
72    let inner_app = rcstr!("INNER_APP");
73    let inner_error = rcstr!("INNER_ERROR");
74    let inner_error_500 = rcstr!("INNER_500");
75
76    let mut replacements = vec![
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 && runtime == NextRuntime::Edge) {
87        replacements.push(("VAR_DEFINITION_PAGE", &*definition_page));
88    }
89    if is_page {
90        replacements.push(("VAR_MODULE_DOCUMENT", &*inner_document));
91        replacements.push(("VAR_MODULE_APP", &*inner_app));
92        if is_page && runtime == NextRuntime::Edge {
93            replacements.push(("VAR_MODULE_GLOBAL_ERROR", &*inner_error));
94        }
95    }
96
97    let pages_structure_ref = pages_structure.await?;
98
99    let (injections, imports) = if is_page && runtime == NextRuntime::Edge {
100        let injections = vec![
101            (
102                "pageRouteModuleOptions",
103                serde_json::to_string(&get_route_module_options(
104                    definition_page.clone(),
105                    definition_pathname.clone(),
106                ))?,
107            ),
108            (
109                "errorRouteModuleOptions",
110                serde_json::to_string(&get_route_module_options(
111                    rcstr!("/_error"),
112                    rcstr!("/_error"),
113                ))?,
114            ),
115            (
116                "user500RouteModuleOptions",
117                serde_json::to_string(&get_route_module_options(rcstr!("/500"), rcstr!("/500")))?,
118            ),
119        ];
120        let imports = vec![
121            // TODO
122            ("incrementalCacheHandler", None),
123            (
124                "userland500Page",
125                pages_structure_ref.error_500.map(|_| &*inner_error_500),
126            ),
127        ];
128        (injections, imports)
129    } else {
130        (vec![], vec![])
131    };
132
133    // Load the file from the next.js codebase.
134    let mut source = load_next_js_template(
135        template_file,
136        project_root.clone(),
137        replacements,
138        injections.iter().map(|(k, v)| (*k, &**v)),
139        imports,
140    )
141    .await?;
142
143    // When we're building the instrumentation page (only when the
144    // instrumentation file conflicts with a page also labeled
145    // /instrumentation) hoist the `register` method.
146    if is_page
147        && (definition_page == "/instrumentation" || definition_page == "/src/instrumentation")
148    {
149        let file = &*file_content_rope(source.content().file_content()).await?;
150
151        let mut result = RopeBuilder::default();
152        result += file;
153
154        writeln!(
155            result,
156            r#"export const register = hoist(userland, "register")"#
157        )?;
158
159        let file = File::from(result.build());
160
161        source = Vc::upcast(VirtualSource::new_with_ident(
162            source.ident(),
163            AssetContent::file(FileContent::Content(file).cell()),
164        ));
165    }
166
167    let mut inner_assets = fxindexmap! {
168        inner => ssr_module,
169    };
170
171    // for PagesData we apply a ?server-data query parameter to avoid conflicts with the Page
172    // module.
173    // We need to copy that to all the modules we create.
174    let source_query = source.ident().await?.query.clone();
175
176    let (app_module, document_module) = if is_page {
177        // We process the document and app modules in the same context and reference type.
178        let document_module = process_global_item(
179            *pages_structure_ref.document,
180            reference_type.clone(),
181            source_query.clone(),
182            ssr_module_context,
183        )
184        .to_resolved()
185        .await?;
186        let app_module = process_global_item(
187            *pages_structure_ref.app,
188            reference_type.clone(),
189            source_query.clone(),
190            ssr_module_context,
191        )
192        .to_resolved()
193        .await?;
194        inner_assets.insert(inner_document, document_module);
195        inner_assets.insert(inner_app, app_module);
196
197        if is_page && runtime == NextRuntime::Edge {
198            inner_assets.insert(
199                inner_error,
200                process_global_item(
201                    *pages_structure_ref.error,
202                    reference_type.clone(),
203                    source_query.clone(),
204                    ssr_module_context,
205                )
206                .to_resolved()
207                .await?,
208            );
209
210            if let Some(error_500) = pages_structure_ref.error_500 {
211                inner_assets.insert(
212                    inner_error_500,
213                    process_global_item(
214                        *error_500,
215                        reference_type.clone(),
216                        source_query.clone(),
217                        ssr_module_context,
218                    )
219                    .to_resolved()
220                    .await?,
221                );
222            }
223        }
224        (Some(app_module), Some(document_module))
225    } else {
226        (None, None)
227    };
228
229    let mut ssr_module = ssr_module_context
230        .process(
231            source,
232            ReferenceType::Internal(ResolvedVc::cell(inner_assets)),
233        )
234        .module();
235
236    if matches!(runtime, NextRuntime::Edge) {
237        ssr_module = wrap_edge_entry(
238            ssr_module_context,
239            project_root,
240            ssr_module,
241            pages_function_name(&definition_page).into(),
242        );
243    }
244
245    Ok(PageSsrEntryModule {
246        ssr_module: ssr_module.to_resolved().await?,
247        app_module,
248        document_module,
249    }
250    .cell())
251}
252
253#[turbo_tasks::function]
254async fn process_global_item(
255    item: Vc<PagesStructureItem>,
256    reference_type: ReferenceType,
257    source_query: RcStr,
258    module_context: Vc<Box<dyn AssetContext>>,
259) -> Result<Vc<Box<dyn Module>>> {
260    let source = Vc::upcast(FileSource::new_with_query(
261        item.file_path().owned().await?,
262        source_query,
263    ));
264    Ok(module_context.process(source, reference_type).module())
265}
266
267#[derive(Serialize)]
268struct PartialRouteModuleOptions {
269    definition: RouteDefinition,
270}
271
272#[derive(Serialize)]
273struct RouteDefinition {
274    kind: RcStr,
275    bundle_path: RcStr,
276    filename: RcStr,
277    /// Describes the pathname including all internal modifiers such as
278    /// intercepting routes, parallel routes and route/page suffixes that are
279    /// not part of the pathname.
280    page: RcStr,
281
282    /// The pathname (including dynamic placeholders) for a route to resolve.
283    pathname: RcStr,
284}
285
286fn get_route_module_options(page: RcStr, pathname: RcStr) -> PartialRouteModuleOptions {
287    PartialRouteModuleOptions {
288        definition: RouteDefinition {
289            kind: rcstr!("PAGES"),
290            page,
291            pathname,
292            // The following aren't used in production.
293            bundle_path: rcstr!(""),
294            filename: rcstr!(""),
295        },
296    }
297}