next_core/next_app/
app_route_entry.rs

1use anyhow::Result;
2use turbo_rcstr::RcStr;
3use turbo_tasks::{ResolvedVc, Value, ValueToString, Vc, fxindexmap};
4use turbo_tasks_fs::FileSystemPath;
5use turbopack::ModuleAssetContext;
6use turbopack_core::{
7    context::AssetContext,
8    module::Module,
9    reference_type::{EntryReferenceSubType, ReferenceType},
10    source::Source,
11};
12
13use crate::{
14    app_segment_config::NextSegmentConfig,
15    next_app::{AppEntry, AppPage, AppPath},
16    next_config::{NextConfig, OutputType},
17    next_edge::entry::wrap_edge_entry,
18    parse_segment_config_from_source,
19    util::{NextRuntime, load_next_js_template},
20};
21
22/// Computes the entry for a Next.js app route.
23/// # Arguments
24///
25/// * `original_segment_config` - A next segment config to be specified explicitly for the given
26///   source.
27/// For some cases `source` may not be the original but the handler (dynamic
28/// metadata) which will lose segment config.
29#[turbo_tasks::function]
30pub async fn get_app_route_entry(
31    nodejs_context: Vc<ModuleAssetContext>,
32    edge_context: Vc<ModuleAssetContext>,
33    source: Vc<Box<dyn Source>>,
34    page: AppPage,
35    project_root: Vc<FileSystemPath>,
36    original_segment_config: Option<Vc<NextSegmentConfig>>,
37    next_config: Vc<NextConfig>,
38) -> Result<Vc<AppEntry>> {
39    let segment_from_source = parse_segment_config_from_source(source);
40    let config = if let Some(original_segment_config) = original_segment_config {
41        let mut segment_config = (*segment_from_source.await?).clone();
42        segment_config.apply_parent_config(&*original_segment_config.await?);
43        segment_config.into()
44    } else {
45        segment_from_source
46    };
47
48    let is_edge = matches!(config.await?.runtime, Some(NextRuntime::Edge));
49    let module_asset_context = if is_edge {
50        edge_context
51    } else {
52        nodejs_context
53    };
54
55    let original_name: RcStr = page.to_string().into();
56    let pathname: RcStr = AppPath::from(page.clone()).to_string().into();
57
58    let path = source.ident().path();
59
60    const INNER: &str = "INNER_APP_ROUTE";
61
62    let output_type: RcStr = next_config
63        .await?
64        .output
65        .as_ref()
66        .map(|o| match o {
67            OutputType::Standalone => "\"standalone\"".to_string(),
68            OutputType::Export => "\"export\"".to_string(),
69        })
70        .map(RcStr::from)
71        .unwrap_or_else(|| "\"\"".into());
72
73    // Load the file from the next.js codebase.
74    let virtual_source = load_next_js_template(
75        "app-route.js",
76        project_root,
77        fxindexmap! {
78            "VAR_DEFINITION_PAGE" => page.to_string().into(),
79            "VAR_DEFINITION_PATHNAME" => pathname.clone(),
80            "VAR_DEFINITION_FILENAME" => path.file_stem().await?.as_ref().unwrap().as_str().into(),
81            // TODO(alexkirsz) Is this necessary?
82            "VAR_DEFINITION_BUNDLE_PATH" => "".to_string().into(),
83            "VAR_RESOLVED_PAGE_PATH" => path.to_string().owned().await?,
84            "VAR_USERLAND" => INNER.into(),
85        },
86        fxindexmap! {
87            "nextConfigOutput" => output_type
88        },
89        fxindexmap! {},
90    )
91    .await?;
92
93    let userland_module = module_asset_context
94        .process(
95            source,
96            Value::new(ReferenceType::Entry(EntryReferenceSubType::AppRoute)),
97        )
98        .module()
99        .to_resolved()
100        .await?;
101
102    let inner_assets = fxindexmap! {
103        INNER.into() => userland_module
104    };
105
106    let mut rsc_entry = module_asset_context
107        .process(
108            Vc::upcast(virtual_source),
109            Value::new(ReferenceType::Internal(ResolvedVc::cell(inner_assets))),
110        )
111        .module();
112
113    if is_edge {
114        rsc_entry = wrap_edge_route(
115            Vc::upcast(module_asset_context),
116            project_root,
117            rsc_entry,
118            page,
119            next_config,
120        );
121    }
122
123    Ok(AppEntry {
124        pathname,
125        original_name,
126        rsc_entry: rsc_entry.to_resolved().await?,
127        config: config.to_resolved().await?,
128    }
129    .cell())
130}
131
132#[turbo_tasks::function]
133async fn wrap_edge_route(
134    asset_context: Vc<Box<dyn AssetContext>>,
135    project_root: Vc<FileSystemPath>,
136    entry: ResolvedVc<Box<dyn Module>>,
137    page: AppPage,
138    next_config: Vc<NextConfig>,
139) -> Result<Vc<Box<dyn Module>>> {
140    const INNER: &str = "INNER_ROUTE_ENTRY";
141
142    let next_config = &*next_config.await?;
143
144    let source = load_next_js_template(
145        "edge-app-route.js",
146        project_root,
147        fxindexmap! {
148            "VAR_USERLAND" => INNER.into(),
149            "VAR_PAGE" => page.to_string().into(),
150        },
151        fxindexmap! {
152            "nextConfig" => serde_json::to_string(next_config)?.into(),
153        },
154        fxindexmap! {},
155    )
156    .await?;
157
158    let inner_assets = fxindexmap! {
159        INNER.into() => entry
160    };
161
162    let wrapped = asset_context
163        .process(
164            Vc::upcast(source),
165            Value::new(ReferenceType::Internal(ResolvedVc::cell(inner_assets))),
166        )
167        .module();
168
169    Ok(wrap_edge_entry(
170        asset_context,
171        project_root,
172        wrapped,
173        AppPath::from(page).to_string().into(),
174    ))
175}