1use anyhow::Result;
2use serde_json::Value as JsonValue;
3use turbo_rcstr::{RcStr, rcstr};
4use turbo_tasks::{FxIndexSet, OperationVc, ResolvedVc, Vc};
5use turbo_tasks_env::ProcessEnv;
6use turbo_tasks_fs::FileSystemPath;
7use turbopack_core::{
8 introspect::{
9 Introspectable, IntrospectableChildren, module::IntrospectableModule,
10 output_asset::IntrospectableOutputAsset,
11 },
12 issue::IssueDescriptionExt,
13 module::Module,
14 output::OutputAsset,
15 version::VersionedContentExt,
16};
17use turbopack_dev_server::{
18 html::DevHtmlAsset,
19 source::{
20 ContentSource, ContentSourceContent, ContentSourceData, ContentSourceDataVary,
21 GetContentSourceContent, ProxyResult,
22 asset_graph::AssetGraphContentSource,
23 conditional::ConditionalContentSource,
24 lazy_instantiated::{GetContentSource, LazyInstantiatedContentSource},
25 route_tree::{BaseSegment, RouteTree, RouteType},
26 },
27};
28
29use super::{
30 RenderData,
31 render_static::{StaticResult, render_static_operation},
32};
33use crate::{
34 external_asset_entrypoints, get_intermediate_asset, node_entry::NodeEntry,
35 route_matcher::RouteMatcher,
36};
37
38#[turbo_tasks::function]
45pub fn create_node_rendered_source(
46 cwd: FileSystemPath,
47 env: ResolvedVc<Box<dyn ProcessEnv>>,
48 base_segments: Vec<BaseSegment>,
49 route_type: RouteType,
50 server_root: FileSystemPath,
51 route_match: ResolvedVc<Box<dyn RouteMatcher>>,
52 pathname: ResolvedVc<RcStr>,
53 entry: ResolvedVc<Box<dyn NodeEntry>>,
54 fallback_page: ResolvedVc<DevHtmlAsset>,
55 render_data: ResolvedVc<JsonValue>,
56 debug: bool,
57) -> Vc<Box<dyn ContentSource>> {
58 let source = NodeRenderContentSource {
59 cwd,
60 env,
61 base_segments,
62 route_type,
63 server_root,
64 route_match,
65 pathname,
66 entry,
67 fallback_page,
68 render_data,
69 debug,
70 }
71 .resolved_cell();
72 Vc::upcast(ConditionalContentSource::new(
73 Vc::upcast(*source),
74 Vc::upcast(
75 LazyInstantiatedContentSource {
76 get_source: ResolvedVc::upcast(source),
77 }
78 .cell(),
79 ),
80 ))
81}
82
83#[turbo_tasks::value]
85pub struct NodeRenderContentSource {
86 cwd: FileSystemPath,
87 env: ResolvedVc<Box<dyn ProcessEnv>>,
88 base_segments: Vec<BaseSegment>,
89 route_type: RouteType,
90 server_root: FileSystemPath,
91 route_match: ResolvedVc<Box<dyn RouteMatcher>>,
92 pathname: ResolvedVc<RcStr>,
93 entry: ResolvedVc<Box<dyn NodeEntry>>,
94 fallback_page: ResolvedVc<DevHtmlAsset>,
95 render_data: ResolvedVc<JsonValue>,
96 debug: bool,
97}
98
99#[turbo_tasks::value_impl]
100impl NodeRenderContentSource {
101 #[turbo_tasks::function]
102 pub fn get_pathname(&self) -> Vc<RcStr> {
103 *self.pathname
104 }
105}
106
107#[turbo_tasks::value_impl]
108impl GetContentSource for NodeRenderContentSource {
109 #[turbo_tasks::function]
112 async fn content_source(&self) -> Result<Vc<Box<dyn ContentSource>>> {
113 let entries = self.entry.entries();
114 let mut set = FxIndexSet::default();
115 for &reference in self.fallback_page.references().await?.iter() {
116 set.insert(reference);
117 }
118 for &entry in entries.await?.iter() {
119 let entry = entry.await?;
120 set.extend(
121 external_asset_entrypoints(
122 *entry.module,
123 *entry.runtime_entries,
124 *entry.chunking_context,
125 entry.intermediate_output_path.clone(),
126 )
127 .await?
128 .iter()
129 .copied(),
130 )
131 }
132 Ok(Vc::upcast(AssetGraphContentSource::new_lazy_multiple(
133 self.server_root.clone(),
134 Vc::cell(set),
135 )))
136 }
137}
138
139#[turbo_tasks::value_impl]
140impl ContentSource for NodeRenderContentSource {
141 #[turbo_tasks::function]
142 async fn get_routes(self: Vc<Self>) -> Result<Vc<RouteTree>> {
143 let this = self.await?;
144 Ok(RouteTree::new_route(
145 this.base_segments.clone(),
146 this.route_type.clone(),
147 Vc::upcast(self),
148 ))
149 }
150}
151
152#[turbo_tasks::value_impl]
153impl GetContentSourceContent for NodeRenderContentSource {
154 #[turbo_tasks::function]
155 fn vary(&self) -> Vc<ContentSourceDataVary> {
156 ContentSourceDataVary {
157 method: true,
158 url: true,
159 original_url: true,
160 raw_headers: true,
161 raw_query: true,
162 ..Default::default()
163 }
164 .cell()
165 }
166
167 #[turbo_tasks::function]
168 async fn get(&self, path: RcStr, data: ContentSourceData) -> Result<Vc<ContentSourceContent>> {
169 let pathname = self.pathname.await?;
170 let Some(params) = &*self.route_match.params(path.clone()).await? else {
171 anyhow::bail!("Non matching path ({}) provided for {}", path, pathname)
172 };
173 let ContentSourceData {
174 method: Some(method),
175 url: Some(url),
176 original_url: Some(original_url),
177 raw_headers: Some(raw_headers),
178 raw_query: Some(raw_query),
179 ..
180 } = &data
181 else {
182 anyhow::bail!("Missing request data")
183 };
184 let entry = (*self.entry).entry(data.clone()).await?;
185 let result_op = render_static_operation(
186 self.cwd.clone(),
187 self.env,
188 self.server_root.join(&path)?,
189 ResolvedVc::upcast(entry.module),
190 entry.runtime_entries,
191 self.fallback_page,
192 entry.chunking_context,
193 entry.intermediate_output_path.clone(),
194 entry.output_root.clone(),
195 entry.project_dir.clone(),
196 RenderData {
197 params: params.clone(),
198 method: method.clone(),
199 url: url.clone(),
200 original_url: original_url.clone(),
201 raw_query: raw_query.clone(),
202 raw_headers: raw_headers.clone(),
203 path: pathname.as_str().into(),
204 data: Some(self.render_data.await?),
205 }
206 .resolved_cell(),
207 self.debug,
208 )
209 .issue_file_path(
210 entry.module.ident().path().await?.clone_value(),
211 format!("server-side rendering {pathname}"),
212 )
213 .await?;
214 Ok(match *result_op.connect().await? {
215 StaticResult::Content {
216 content,
217 status_code,
218 headers,
219 } => ContentSourceContent::static_with_headers(
220 content.versioned(),
221 status_code,
222 *headers,
223 ),
224 StaticResult::StreamedContent {
225 status,
226 headers,
227 ref body,
228 } => {
229 ContentSourceContent::HttpProxy(static_streamed_content_to_proxy_result_operation(
230 result_op,
231 ProxyResult {
232 status,
233 headers: headers.owned().await?,
234 body: body.clone(),
235 }
236 .resolved_cell(),
237 ))
238 .cell()
239 }
240 StaticResult::Rewrite(rewrite) => ContentSourceContent::Rewrite(rewrite).cell(),
241 })
242 }
243}
244
245#[turbo_tasks::function(operation)]
246async fn static_streamed_content_to_proxy_result_operation(
247 result_op: OperationVc<StaticResult>,
248 proxy_result: ResolvedVc<ProxyResult>,
249) -> Result<Vc<ProxyResult>> {
250 let _ = result_op.connect().await?;
254 Ok(*proxy_result)
255}
256
257#[turbo_tasks::value_impl]
258impl Introspectable for NodeRenderContentSource {
259 #[turbo_tasks::function]
260 fn ty(&self) -> Vc<RcStr> {
261 Vc::cell(rcstr!("node render content source"))
262 }
263
264 #[turbo_tasks::function]
265 fn title(&self) -> Vc<RcStr> {
266 *self.pathname
267 }
268
269 #[turbo_tasks::function]
270 fn details(&self) -> Vc<RcStr> {
271 Vc::cell(
272 format!(
273 "base: {:?}\ntype: {:?}",
274 self.base_segments, self.route_type
275 )
276 .into(),
277 )
278 }
279
280 #[turbo_tasks::function]
281 async fn children(&self) -> Result<Vc<IntrospectableChildren>> {
282 let mut set = FxIndexSet::default();
283 for &entry in self.entry.entries().await?.iter() {
284 let entry = entry.await?;
285 set.insert((
286 rcstr!("module"),
287 IntrospectableModule::new(Vc::upcast(*entry.module))
288 .to_resolved()
289 .await?,
290 ));
291 set.insert((
292 rcstr!("intermediate asset"),
293 IntrospectableOutputAsset::new(get_intermediate_asset(
294 *entry.chunking_context,
295 *entry.module,
296 *entry.runtime_entries,
297 ))
298 .to_resolved()
299 .await?,
300 ));
301 }
302 Ok(Vc::cell(set))
303 }
304}