turbopack_node/render/
node_api_source.rs

1use anyhow::Result;
2use serde_json::Value as JsonValue;
3use turbo_rcstr::{RcStr, rcstr};
4use turbo_tasks::{FxIndexSet, ResolvedVc, Vc};
5use turbo_tasks_env::ProcessEnv;
6use turbo_tasks_fs::FileSystemPath;
7use turbopack_core::introspect::{
8    Introspectable, IntrospectableChildren, module::IntrospectableModule,
9    output_asset::IntrospectableOutputAsset,
10};
11use turbopack_dev_server::source::{
12    ContentSource, ContentSourceContent, ContentSourceData, ContentSourceDataVary,
13    GetContentSourceContent,
14    route_tree::{BaseSegment, RouteTree, RouteType},
15};
16
17use super::{RenderData, render_proxy::render_proxy_operation};
18use crate::{get_intermediate_asset, node_entry::NodeEntry, route_matcher::RouteMatcher};
19
20/// Creates a [NodeApiContentSource].
21#[turbo_tasks::function]
22pub fn create_node_api_source(
23    cwd: FileSystemPath,
24    env: ResolvedVc<Box<dyn ProcessEnv>>,
25    base_segments: Vec<BaseSegment>,
26    route_type: RouteType,
27    server_root: FileSystemPath,
28    route_match: ResolvedVc<Box<dyn RouteMatcher>>,
29    pathname: ResolvedVc<RcStr>,
30    entry: ResolvedVc<Box<dyn NodeEntry>>,
31    render_data: ResolvedVc<JsonValue>,
32    debug: bool,
33) -> Vc<Box<dyn ContentSource>> {
34    Vc::upcast(
35        NodeApiContentSource {
36            cwd,
37            env,
38            base_segments,
39            route_type,
40            server_root,
41            pathname,
42            route_match,
43            entry,
44            render_data,
45            debug,
46        }
47        .cell(),
48    )
49}
50
51/// A content source that proxies API requests to one-off Node.js
52/// servers running the passed `entry` when it matches a `path_regex`.
53///
54/// It needs a temporary directory (`intermediate_output_path`) to place file
55/// for Node.js execution during rendering. The `chunking_context` should emit
56/// to this directory.
57#[turbo_tasks::value]
58pub struct NodeApiContentSource {
59    cwd: FileSystemPath,
60    env: ResolvedVc<Box<dyn ProcessEnv>>,
61    base_segments: Vec<BaseSegment>,
62    route_type: RouteType,
63    server_root: FileSystemPath,
64    pathname: ResolvedVc<RcStr>,
65    route_match: ResolvedVc<Box<dyn RouteMatcher>>,
66    entry: ResolvedVc<Box<dyn NodeEntry>>,
67    render_data: ResolvedVc<JsonValue>,
68    debug: bool,
69}
70
71#[turbo_tasks::value_impl]
72impl NodeApiContentSource {
73    #[turbo_tasks::function]
74    pub fn get_pathname(&self) -> Vc<RcStr> {
75        *self.pathname
76    }
77}
78
79#[turbo_tasks::value_impl]
80impl ContentSource for NodeApiContentSource {
81    #[turbo_tasks::function]
82    async fn get_routes(self: Vc<Self>) -> Result<Vc<RouteTree>> {
83        let this = self.await?;
84        Ok(RouteTree::new_route(
85            this.base_segments.clone(),
86            this.route_type.clone(),
87            Vc::upcast(self),
88        ))
89    }
90}
91
92#[turbo_tasks::value_impl]
93impl GetContentSourceContent for NodeApiContentSource {
94    #[turbo_tasks::function]
95    fn vary(&self) -> Vc<ContentSourceDataVary> {
96        ContentSourceDataVary {
97            method: true,
98            url: true,
99            original_url: true,
100            raw_headers: true,
101            raw_query: true,
102            body: true,
103            cache_buster: true,
104            ..Default::default()
105        }
106        .cell()
107    }
108
109    #[turbo_tasks::function]
110    async fn get(&self, path: RcStr, data: ContentSourceData) -> Result<Vc<ContentSourceContent>> {
111        let Some(params) = &*self.route_match.params(path.clone()).await? else {
112            anyhow::bail!("Non matching path provided")
113        };
114        let ContentSourceData {
115            method: Some(method),
116            url: Some(url),
117            original_url: Some(original_url),
118            raw_headers: Some(raw_headers),
119            raw_query: Some(raw_query),
120            body: Some(body),
121            ..
122        } = &data
123        else {
124            anyhow::bail!("Missing request data")
125        };
126        let entry = (*self.entry).entry(data.clone()).await?;
127        Ok(ContentSourceContent::HttpProxy(render_proxy_operation(
128            self.cwd.clone(),
129            self.env,
130            self.server_root.join(&path.clone())?,
131            ResolvedVc::upcast(entry.module),
132            entry.runtime_entries,
133            entry.chunking_context,
134            entry.intermediate_output_path.clone(),
135            entry.output_root.clone(),
136            entry.project_dir.clone(),
137            RenderData {
138                params: params.clone(),
139                method: method.clone(),
140                url: url.clone(),
141                original_url: original_url.clone(),
142                raw_query: raw_query.clone(),
143                raw_headers: raw_headers.clone(),
144                path: format!("/{path}").into(),
145                data: Some(self.render_data.await?),
146            }
147            .resolved_cell(),
148            *body,
149            self.debug,
150        ))
151        .cell())
152    }
153}
154
155#[turbo_tasks::value_impl]
156impl Introspectable for NodeApiContentSource {
157    #[turbo_tasks::function]
158    fn ty(&self) -> Vc<RcStr> {
159        Vc::cell(rcstr!("node api content source"))
160    }
161
162    #[turbo_tasks::function]
163    fn title(&self) -> Vc<RcStr> {
164        *self.pathname
165    }
166
167    #[turbo_tasks::function]
168    fn details(&self) -> Vc<RcStr> {
169        Vc::cell(
170            format!(
171                "base: {:?}\ntype: {:?}",
172                self.base_segments, self.route_type
173            )
174            .into(),
175        )
176    }
177
178    #[turbo_tasks::function]
179    async fn children(&self) -> Result<Vc<IntrospectableChildren>> {
180        let mut set = FxIndexSet::default();
181        for &entry in self.entry.entries().await?.iter() {
182            let entry = entry.await?;
183            set.insert((
184                rcstr!("module"),
185                IntrospectableModule::new(Vc::upcast(*entry.module))
186                    .to_resolved()
187                    .await?,
188            ));
189            set.insert((
190                rcstr!("intermediate asset"),
191                IntrospectableOutputAsset::new(get_intermediate_asset(
192                    *entry.chunking_context,
193                    Vc::upcast(*entry.module),
194                    *entry.runtime_entries,
195                ))
196                .to_resolved()
197                .await?,
198            ));
199        }
200        Ok(Vc::cell(set))
201    }
202}