next_core/next_app/
app_page_entry.rs

1use std::io::Write;
2
3use anyhow::Result;
4use turbo_rcstr::RcStr;
5use turbo_tasks::{ResolvedVc, Vc, fxindexmap};
6use turbo_tasks_fs::{self, File, FileSystemPath, rope::RopeBuilder};
7use turbopack::ModuleAssetContext;
8use turbopack_core::{
9    asset::{Asset, AssetContent},
10    context::AssetContext,
11    module::Module,
12    reference_type::ReferenceType,
13    source::Source,
14    virtual_source::VirtualSource,
15};
16use turbopack_ecmascript::runtime_functions::{TURBOPACK_LOAD, TURBOPACK_REQUIRE};
17
18use super::app_entry::AppEntry;
19use crate::{
20    app_page_loader_tree::{AppPageLoaderTreeModule, GLOBAL_ERROR},
21    app_structure::AppPageLoaderTree,
22    next_app::{AppPage, AppPath},
23    next_config::NextConfig,
24    next_edge::entry::wrap_edge_entry,
25    next_server_component::NextServerComponentTransition,
26    parse_segment_config_from_loader_tree,
27    util::{NextRuntime, app_function_name, file_content_rope, load_next_js_template},
28};
29
30/// Computes the entry for a Next.js app page.
31#[turbo_tasks::function]
32pub async fn get_app_page_entry(
33    nodejs_context: ResolvedVc<ModuleAssetContext>,
34    edge_context: ResolvedVc<ModuleAssetContext>,
35    loader_tree: Vc<AppPageLoaderTree>,
36    page: AppPage,
37    project_root: FileSystemPath,
38    next_config: Vc<NextConfig>,
39) -> Result<Vc<AppEntry>> {
40    let config = parse_segment_config_from_loader_tree(loader_tree);
41    let is_edge = matches!(config.await?.runtime, Some(NextRuntime::Edge));
42    let module_asset_context = if is_edge {
43        edge_context
44    } else {
45        nodejs_context
46    };
47
48    let server_component_transition =
49        ResolvedVc::upcast(NextServerComponentTransition::new().to_resolved().await?);
50
51    let base_path = next_config.await?.base_path.clone();
52    let loader_tree = AppPageLoaderTreeModule::build(
53        loader_tree,
54        module_asset_context,
55        server_component_transition,
56        base_path,
57    )
58    .await?;
59
60    let AppPageLoaderTreeModule {
61        inner_assets,
62        imports,
63        loader_tree_code,
64    } = loader_tree;
65
66    let mut result = RopeBuilder::default();
67
68    for import in imports {
69        writeln!(result, "{import}")?;
70    }
71
72    let original_name: RcStr = page.to_string().into();
73    let pathname: RcStr = AppPath::from(page.clone()).to_string().into();
74
75    // Load the file from the next.js codebase.
76    let source = load_next_js_template(
77        "app-page.js",
78        project_root.clone(),
79        &[
80            ("VAR_DEFINITION_PAGE", &*page.to_string()),
81            ("VAR_DEFINITION_PATHNAME", &pathname),
82            (
83                "VAR_MODULE_GLOBAL_ERROR",
84                if inner_assets.contains_key(GLOBAL_ERROR) {
85                    GLOBAL_ERROR
86                } else {
87                    "next/dist/client/components/builtin/global-error"
88                },
89            ),
90        ],
91        &[
92            ("tree", &*loader_tree_code),
93            ("__next_app_require__", &TURBOPACK_REQUIRE.bound()),
94            ("__next_app_load_chunk__", &TURBOPACK_LOAD.bound()),
95        ],
96        &[],
97    )
98    .await?;
99
100    let source_content = &*file_content_rope(source.content().file_content()).await?;
101
102    result.concat(source_content);
103
104    let query = qstring::QString::new(vec![("page", page.to_string())]);
105
106    let file = File::from(result.build());
107    let source = VirtualSource::new_with_ident(
108        source.ident().with_query(RcStr::from(format!("?{query}"))),
109        AssetContent::file(file.into()),
110    );
111
112    let mut rsc_entry = module_asset_context
113        .process(
114            Vc::upcast(source),
115            ReferenceType::Internal(ResolvedVc::cell(inner_assets)),
116        )
117        .module();
118
119    if is_edge {
120        rsc_entry = wrap_edge_page(
121            *ResolvedVc::upcast(module_asset_context),
122            project_root.clone(),
123            rsc_entry,
124            page,
125            next_config,
126        );
127    };
128
129    Ok(AppEntry {
130        pathname,
131        original_name,
132        rsc_entry: rsc_entry.to_resolved().await?,
133        config: config.to_resolved().await?,
134    }
135    .cell())
136}
137
138#[turbo_tasks::function]
139async fn wrap_edge_page(
140    asset_context: Vc<Box<dyn AssetContext>>,
141    project_root: FileSystemPath,
142    entry: ResolvedVc<Box<dyn Module>>,
143    page: AppPage,
144    next_config: Vc<NextConfig>,
145) -> Result<Vc<Box<dyn Module>>> {
146    const INNER: &str = "INNER_PAGE_ENTRY";
147
148    let next_config_val = &*next_config.await?;
149
150    let source = load_next_js_template(
151        "edge-ssr-app.js",
152        project_root.clone(),
153        &[("VAR_USERLAND", INNER), ("VAR_PAGE", &page.to_string())],
154        &[
155            // TODO do we really need to pass the entire next config here?
156            // This is bad for invalidation as any config change will invalidate this
157            ("nextConfig", &*serde_json::to_string(next_config_val)?),
158        ],
159        &[("incrementalCacheHandler", None)],
160    )
161    .await?;
162
163    let inner_assets = fxindexmap! {
164        INNER.into() => entry
165    };
166
167    let wrapped = asset_context
168        .process(
169            Vc::upcast(source),
170            ReferenceType::Internal(ResolvedVc::cell(inner_assets)),
171        )
172        .module();
173
174    Ok(wrap_edge_entry(
175        asset_context,
176        project_root,
177        wrapped,
178        app_function_name(&page).into(),
179    ))
180}