next_core/next_app/
app_page_entry.rs

1use std::io::Write;
2
3use anyhow::Result;
4use turbo_rcstr::RcStr;
5use turbo_tasks::{ResolvedVc, TryJoinIterExt, Value, ValueToString, 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::{
17    runtime_functions::{TURBOPACK_LOAD, TURBOPACK_REQUIRE},
18    utils::StringifyJs,
19};
20
21use super::app_entry::AppEntry;
22use crate::{
23    app_page_loader_tree::{AppPageLoaderTreeModule, GLOBAL_ERROR},
24    app_structure::AppPageLoaderTree,
25    next_app::{AppPage, AppPath},
26    next_config::NextConfig,
27    next_edge::entry::wrap_edge_entry,
28    next_server_component::NextServerComponentTransition,
29    parse_segment_config_from_loader_tree,
30    util::{NextRuntime, file_content_rope, load_next_js_template},
31};
32
33/// Computes the entry for a Next.js app page.
34#[turbo_tasks::function]
35pub async fn get_app_page_entry(
36    nodejs_context: ResolvedVc<ModuleAssetContext>,
37    edge_context: ResolvedVc<ModuleAssetContext>,
38    loader_tree: Vc<AppPageLoaderTree>,
39    page: AppPage,
40    project_root: Vc<FileSystemPath>,
41    next_config: Vc<NextConfig>,
42) -> Result<Vc<AppEntry>> {
43    let config = parse_segment_config_from_loader_tree(loader_tree);
44    let is_edge = matches!(config.await?.runtime, Some(NextRuntime::Edge));
45    let module_asset_context = if is_edge {
46        edge_context
47    } else {
48        nodejs_context
49    };
50
51    let server_component_transition =
52        ResolvedVc::upcast(NextServerComponentTransition::new().to_resolved().await?);
53
54    let base_path = next_config.await?.base_path.clone();
55    let loader_tree = AppPageLoaderTreeModule::build(
56        loader_tree,
57        module_asset_context,
58        server_component_transition,
59        base_path,
60    )
61    .await?;
62
63    let AppPageLoaderTreeModule {
64        inner_assets,
65        imports,
66        loader_tree_code,
67        pages,
68    } = loader_tree;
69
70    let mut result = RopeBuilder::default();
71
72    for import in imports {
73        writeln!(result, "{import}")?;
74    }
75
76    let pages = pages.iter().map(|page| page.to_string()).try_join().await?;
77
78    let original_name: RcStr = page.to_string().into();
79    let pathname: RcStr = AppPath::from(page.clone()).to_string().into();
80
81    // Load the file from the next.js codebase.
82    let source = load_next_js_template(
83        "app-page.js",
84        project_root,
85        fxindexmap! {
86            "VAR_DEFINITION_PAGE" => page.to_string().into(),
87            "VAR_DEFINITION_PATHNAME" => pathname.clone(),
88            "VAR_MODULE_GLOBAL_ERROR" => if inner_assets.contains_key(GLOBAL_ERROR) {
89                GLOBAL_ERROR.into()
90             } else {
91                "next/dist/client/components/global-error".into()
92            },
93        },
94        fxindexmap! {
95            "tree" => loader_tree_code,
96            "pages" => StringifyJs(&pages).to_string().into(),
97            "__next_app_require__" => TURBOPACK_REQUIRE.full.into(),
98            "__next_app_load_chunk__" => TURBOPACK_LOAD.full.into(),
99        },
100        fxindexmap! {},
101    )
102    .await?;
103
104    let source_content = &*file_content_rope(source.content().file_content()).await?;
105
106    result.concat(source_content);
107
108    let query = qstring::QString::new(vec![("page", page.to_string())]);
109
110    let file = File::from(result.build());
111    let source = VirtualSource::new_with_ident(
112        source
113            .ident()
114            .with_query(Vc::cell(format!("?{query}").into())),
115        AssetContent::file(file.into()),
116    );
117
118    let mut rsc_entry = module_asset_context
119        .process(
120            Vc::upcast(source),
121            Value::new(ReferenceType::Internal(ResolvedVc::cell(inner_assets))),
122        )
123        .module();
124
125    if is_edge {
126        rsc_entry = wrap_edge_page(
127            *ResolvedVc::upcast(module_asset_context),
128            project_root,
129            rsc_entry,
130            page,
131            next_config,
132        );
133    };
134
135    Ok(AppEntry {
136        pathname,
137        original_name,
138        rsc_entry: rsc_entry.to_resolved().await?,
139        config: config.to_resolved().await?,
140    }
141    .cell())
142}
143
144#[turbo_tasks::function]
145async fn wrap_edge_page(
146    asset_context: Vc<Box<dyn AssetContext>>,
147    project_root: Vc<FileSystemPath>,
148    entry: ResolvedVc<Box<dyn Module>>,
149    page: AppPage,
150    next_config: Vc<NextConfig>,
151) -> Result<Vc<Box<dyn Module>>> {
152    const INNER: &str = "INNER_PAGE_ENTRY";
153
154    let next_config_val = &*next_config.await?;
155
156    // TODO(WEB-1824): add build support
157    let dev = true;
158
159    // TODO(timneutkens): remove this
160    let is_server_component = true;
161
162    let server_actions = next_config.experimental_server_actions().await?;
163
164    let sri_enabled = !dev
165        && next_config
166            .experimental_sri()
167            .await?
168            .as_ref()
169            .map(|sri| sri.algorithm.as_ref())
170            .is_some();
171
172    let source = load_next_js_template(
173        "edge-ssr-app.js",
174        project_root,
175        fxindexmap! {
176            "VAR_USERLAND" => INNER.into(),
177            "VAR_PAGE" => page.to_string().into(),
178        },
179        fxindexmap! {
180            "sriEnabled" => serde_json::Value::Bool(sri_enabled).to_string().into(),
181            // TODO do we really need to pass the entire next config here?
182            // This is bad for invalidation as any config change will invalidate this
183            "nextConfig" => serde_json::to_string(next_config_val)?.into(),
184            "isServerComponent" => serde_json::Value::Bool(is_server_component).to_string().into(),
185            "dev" => serde_json::Value::Bool(dev).to_string().into(),
186            "serverActions" => serde_json::to_string(&server_actions)?.into(),
187        },
188        fxindexmap! {
189            "incrementalCacheHandler" => None,
190        },
191    )
192    .await?;
193
194    let inner_assets = fxindexmap! {
195        INNER.into() => entry
196    };
197
198    let wrapped = asset_context
199        .process(
200            Vc::upcast(source),
201            Value::new(ReferenceType::Internal(ResolvedVc::cell(inner_assets))),
202        )
203        .module();
204
205    Ok(wrap_edge_entry(
206        asset_context,
207        project_root,
208        wrapped,
209        AppPath::from(page).to_string().into(),
210    ))
211}