next_core/next_pages/
page_entry.rs

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