turbopack_node/render/
rendered_source.rs

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/// 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: 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/// see [create_node_rendered_source]
84#[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    /// 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.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    // we already assume `result_op`'s value here because we're called inside of a match arm, but
251    // await `result_op` anyways, so that if it generates any collectible issues, they're captured
252    // here.
253    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}