turbopack_node/render/
node_api_source.rs

1use anyhow::Result;
2use serde_json::Value as JsonValue;
3use turbo_rcstr::RcStr;
4use turbo_tasks::{FxIndexSet, ResolvedVc, Value, 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: ResolvedVc<FileSystemPath>,
24    env: ResolvedVc<Box<dyn ProcessEnv>>,
25    base_segments: Vec<BaseSegment>,
26    route_type: RouteType,
27    server_root: ResolvedVc<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: ResolvedVc<FileSystemPath>,
60    env: ResolvedVc<Box<dyn ProcessEnv>>,
61    base_segments: Vec<BaseSegment>,
62    route_type: RouteType,
63    server_root: ResolvedVc<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(
111        &self,
112        path: RcStr,
113        data: Value<ContentSourceData>,
114    ) -> Result<Vc<ContentSourceContent>> {
115        let Some(params) = &*self.route_match.params(path.clone()).await? else {
116            anyhow::bail!("Non matching path provided")
117        };
118        let ContentSourceData {
119            method: Some(method),
120            url: Some(url),
121            original_url: Some(original_url),
122            raw_headers: Some(raw_headers),
123            raw_query: Some(raw_query),
124            body: Some(body),
125            ..
126        } = &*data
127        else {
128            anyhow::bail!("Missing request data")
129        };
130        let entry = (*self.entry).entry(data.clone()).await?;
131        Ok(ContentSourceContent::HttpProxy(render_proxy_operation(
132            self.cwd,
133            self.env,
134            self.server_root.join(path.clone()).to_resolved().await?,
135            ResolvedVc::upcast(entry.module),
136            entry.runtime_entries,
137            entry.chunking_context,
138            entry.intermediate_output_path,
139            entry.output_root,
140            entry.project_dir,
141            RenderData {
142                params: params.clone(),
143                method: method.clone(),
144                url: url.clone(),
145                original_url: original_url.clone(),
146                raw_query: raw_query.clone(),
147                raw_headers: raw_headers.clone(),
148                path: format!("/{path}").into(),
149                data: Some(self.render_data.await?),
150            }
151            .resolved_cell(),
152            *body,
153            self.debug,
154        ))
155        .cell())
156    }
157}
158
159#[turbo_tasks::function]
160fn introspectable_type() -> Vc<RcStr> {
161    Vc::cell("node api content source".into())
162}
163
164#[turbo_tasks::value_impl]
165impl Introspectable for NodeApiContentSource {
166    #[turbo_tasks::function]
167    fn ty(&self) -> Vc<RcStr> {
168        introspectable_type()
169    }
170
171    #[turbo_tasks::function]
172    fn title(&self) -> Vc<RcStr> {
173        *self.pathname
174    }
175
176    #[turbo_tasks::function]
177    async fn details(&self) -> Vc<RcStr> {
178        Vc::cell(
179            format!(
180                "base: {:?}\ntype: {:?}",
181                self.base_segments, self.route_type
182            )
183            .into(),
184        )
185    }
186
187    #[turbo_tasks::function]
188    async fn children(&self) -> Result<Vc<IntrospectableChildren>> {
189        let mut set = FxIndexSet::default();
190        for &entry in self.entry.entries().await?.iter() {
191            let entry = entry.await?;
192            set.insert((
193                ResolvedVc::cell("module".into()),
194                IntrospectableModule::new(Vc::upcast(*entry.module))
195                    .to_resolved()
196                    .await?,
197            ));
198            set.insert((
199                ResolvedVc::cell("intermediate asset".into()),
200                IntrospectableOutputAsset::new(get_intermediate_asset(
201                    *entry.chunking_context,
202                    Vc::upcast(*entry.module),
203                    *entry.runtime_entries,
204                ))
205                .to_resolved()
206                .await?,
207            ));
208        }
209        Ok(Vc::cell(set))
210    }
211}