1use anyhow::Result;
2use serde_json::Value as JsonValue;
3use turbo_rcstr::RcStr;
4use turbo_tasks::{FxIndexSet, OperationVc, ResolvedVc, Value, 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: ResolvedVc<FileSystemPath>,
47 env: ResolvedVc<Box<dyn ProcessEnv>>,
48 base_segments: Vec<BaseSegment>,
49 route_type: RouteType,
50 server_root: ResolvedVc<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: ResolvedVc<FileSystemPath>,
87 env: ResolvedVc<Box<dyn ProcessEnv>>,
88 base_segments: Vec<BaseSegment>,
89 route_type: RouteType,
90 server_root: ResolvedVc<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,
126 )
127 .await?
128 .iter()
129 .copied(),
130 )
131 }
132 Ok(Vc::upcast(AssetGraphContentSource::new_lazy_multiple(
133 *self.server_root,
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(
169 &self,
170 path: RcStr,
171 data: Value<ContentSourceData>,
172 ) -> Result<Vc<ContentSourceContent>> {
173 let pathname = self.pathname.await?;
174 let Some(params) = &*self.route_match.params(path.clone()).await? else {
175 anyhow::bail!("Non matching path ({}) provided for {}", path, pathname)
176 };
177 let ContentSourceData {
178 method: Some(method),
179 url: Some(url),
180 original_url: Some(original_url),
181 raw_headers: Some(raw_headers),
182 raw_query: Some(raw_query),
183 ..
184 } = &*data
185 else {
186 anyhow::bail!("Missing request data")
187 };
188 let entry = (*self.entry).entry(data.clone()).await?;
189 let result_op = render_static_operation(
190 self.cwd,
191 self.env,
192 self.server_root.join(path.clone()).to_resolved().await?,
193 ResolvedVc::upcast(entry.module),
194 entry.runtime_entries,
195 self.fallback_page,
196 entry.chunking_context,
197 entry.intermediate_output_path,
198 entry.output_root,
199 entry.project_dir,
200 RenderData {
201 params: params.clone(),
202 method: method.clone(),
203 url: url.clone(),
204 original_url: original_url.clone(),
205 raw_query: raw_query.clone(),
206 raw_headers: raw_headers.clone(),
207 path: pathname.as_str().into(),
208 data: Some(self.render_data.await?),
209 }
210 .resolved_cell(),
211 self.debug,
212 )
213 .issue_file_path(
214 entry.module.ident().path(),
215 format!("server-side rendering {pathname}"),
216 )
217 .await?;
218 Ok(match *result_op.connect().await? {
219 StaticResult::Content {
220 content,
221 status_code,
222 headers,
223 } => ContentSourceContent::static_with_headers(
224 content.versioned(),
225 status_code,
226 *headers,
227 ),
228 StaticResult::StreamedContent {
229 status,
230 headers,
231 ref body,
232 } => {
233 ContentSourceContent::HttpProxy(static_streamed_content_to_proxy_result_operation(
234 result_op,
235 ProxyResult {
236 status,
237 headers: headers.owned().await?,
238 body: body.clone(),
239 }
240 .resolved_cell(),
241 ))
242 .cell()
243 }
244 StaticResult::Rewrite(rewrite) => ContentSourceContent::Rewrite(rewrite).cell(),
245 })
246 }
247}
248
249#[turbo_tasks::function(operation)]
250async fn static_streamed_content_to_proxy_result_operation(
251 result_op: OperationVc<StaticResult>,
252 proxy_result: ResolvedVc<ProxyResult>,
253) -> Result<Vc<ProxyResult>> {
254 let _ = result_op.connect().await?;
258 Ok(*proxy_result)
259}
260
261#[turbo_tasks::function]
262fn introspectable_type() -> Vc<RcStr> {
263 Vc::cell("node render content source".into())
264}
265
266#[turbo_tasks::value_impl]
267impl Introspectable for NodeRenderContentSource {
268 #[turbo_tasks::function]
269 fn ty(&self) -> Vc<RcStr> {
270 introspectable_type()
271 }
272
273 #[turbo_tasks::function]
274 fn title(&self) -> Vc<RcStr> {
275 *self.pathname
276 }
277
278 #[turbo_tasks::function]
279 async fn details(&self) -> Vc<RcStr> {
280 Vc::cell(
281 format!(
282 "base: {:?}\ntype: {:?}",
283 self.base_segments, self.route_type
284 )
285 .into(),
286 )
287 }
288
289 #[turbo_tasks::function]
290 async fn children(&self) -> Result<Vc<IntrospectableChildren>> {
291 let mut set = FxIndexSet::default();
292 for &entry in self.entry.entries().await?.iter() {
293 let entry = entry.await?;
294 set.insert((
295 ResolvedVc::cell("module".into()),
296 IntrospectableModule::new(Vc::upcast(*entry.module))
297 .to_resolved()
298 .await?,
299 ));
300 set.insert((
301 ResolvedVc::cell("intermediate asset".into()),
302 IntrospectableOutputAsset::new(get_intermediate_asset(
303 *entry.chunking_context,
304 *entry.module,
305 *entry.runtime_entries,
306 ))
307 .to_resolved()
308 .await?,
309 ));
310 }
311 Ok(Vc::cell(set))
312 }
313}