turbopack_node/render/
rendered_source.rs

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/// Creates a content source that renders something in Node.js with the passed
39/// `entry` when it matches a `path_regex`. Once rendered it serves
40/// all assets referenced by the `entry` that are within the `server_root`.
41/// It needs a temporary directory (`intermediate_output_path`) to place file
42/// for Node.js execution during rendering. The `chunking_context` should emit
43/// to this directory.
44#[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/// see [create_node_rendered_source]
84#[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    /// Returns the [ContentSource] that serves all referenced external
110    /// assets. This is wrapped into [LazyInstantiatedContentSource].
111    #[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    // we already assume `result_op`'s value here because we're called inside of a match arm, but
255    // await `result_op` anyways, so that if it generates any collectible issues, they're captured
256    // here.
257    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}