next_napi_bindings/next_api/
endpoint.rs1use std::{ops::Deref, sync::Arc};
2
3use anyhow::Result;
4use futures_util::TryFutureExt;
5use napi::{JsFunction, bindgen_prelude::External};
6use napi_derive::napi;
7use next_api::{
8 operation::OptionEndpoint,
9 paths::AssetPath,
10 route::{
11 Endpoint, EndpointOutputPaths, endpoint_client_changed_operation,
12 endpoint_server_changed_operation, endpoint_write_to_disk_operation,
13 },
14};
15use tracing::Instrument;
16use turbo_rcstr::RcStr;
17use turbo_tasks::{
18 Completion, Effects, OperationVc, ReadRef, Vc, read_strongly_consistent_and_apply_effects,
19};
20use turbopack_core::issue::{IssueFilter, PlainIssue};
21
22use crate::next_api::utils::{
23 DetachedVc, NapiIssue, RootTask, TurbopackResult, strongly_consistent_catch_collectables,
24 subscribe,
25};
26
27#[napi(object)]
28#[derive(Default)]
29pub struct NapiEndpointConfig {}
30
31#[napi(object)]
32#[derive(Default)]
33pub struct NapiAssetPath {
34 pub path: RcStr,
35 pub content_hash: RcStr,
36}
37
38impl From<AssetPath> for NapiAssetPath {
39 fn from(asset_path: AssetPath) -> Self {
40 Self {
41 path: asset_path.path,
42 content_hash: asset_path.content_hash,
43 }
44 }
45}
46
47#[napi(object)]
48#[derive(Default)]
49pub struct NapiWrittenEndpoint {
50 pub r#type: String,
51 pub entry_path: Option<String>,
52 pub client_paths: Vec<String>,
53 pub server_paths: Vec<NapiAssetPath>,
54 pub config: NapiEndpointConfig,
55}
56
57impl From<Option<EndpointOutputPaths>> for NapiWrittenEndpoint {
58 fn from(written_endpoint: Option<EndpointOutputPaths>) -> Self {
59 match written_endpoint {
60 Some(EndpointOutputPaths::NodeJs {
61 server_entry_path,
62 server_paths,
63 client_paths,
64 }) => Self {
65 r#type: "nodejs".to_string(),
66 entry_path: Some(server_entry_path.into_owned()),
67 client_paths: client_paths.into_iter().map(From::from).collect(),
68 server_paths: server_paths.into_iter().map(From::from).collect(),
69 ..Default::default()
70 },
71 Some(EndpointOutputPaths::Edge {
72 server_paths,
73 client_paths,
74 }) => Self {
75 r#type: "edge".to_string(),
76 client_paths: client_paths.into_iter().map(From::from).collect(),
77 server_paths: server_paths.into_iter().map(From::from).collect(),
78 ..Default::default()
79 },
80 Some(EndpointOutputPaths::NotFound) | None => Self {
81 r#type: "none".to_string(),
82 ..Default::default()
83 },
84 }
85 }
86}
87
88pub struct ExternalEndpoint(pub DetachedVc<OptionEndpoint>);
95
96impl Deref for ExternalEndpoint {
97 type Target = DetachedVc<OptionEndpoint>;
98
99 fn deref(&self) -> &Self::Target {
100 &self.0
101 }
102}
103
104async fn issue_filter_from_endpoint(
113 endpoint_op: OperationVc<OptionEndpoint>,
114) -> ReadRef<IssueFilter> {
115 if let Ok(ep_option) = endpoint_op.connect().await
116 && let Some(ep) = &*ep_option
117 && let Ok(filter) = ep.project().issue_filter().await
118 {
119 filter
120 } else {
121 ReadRef::new_owned(IssueFilter::warnings_and_foreign_errors())
122 }
123}
124
125#[turbo_tasks::value(serialization = "skip")]
126struct WrittenEndpointWithIssues {
127 written: Option<ReadRef<EndpointOutputPaths>>,
128 issues: Arc<Vec<ReadRef<PlainIssue>>>,
129 effects: Arc<Effects>,
130}
131
132#[turbo_tasks::function(operation, root)]
133async fn get_written_endpoint_with_issues_operation(
134 endpoint_op: OperationVc<OptionEndpoint>,
135) -> Result<Vc<WrittenEndpointWithIssues>> {
136 let write_to_disk_op = endpoint_write_to_disk_operation(endpoint_op);
137 let filter = issue_filter_from_endpoint(endpoint_op).await;
138 let (written, issues, effects) =
139 strongly_consistent_catch_collectables(write_to_disk_op, &filter).await?;
140 Ok(WrittenEndpointWithIssues {
141 written,
142 issues,
143 effects,
144 }
145 .cell())
146}
147
148#[tracing::instrument(level = "info", name = "write endpoint to disk", skip_all)]
149#[napi]
150pub async fn endpoint_write_to_disk(
151 #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
152) -> napi::Result<TurbopackResult<NapiWrittenEndpoint>> {
153 let ctx = endpoint.turbopack_ctx();
154 let endpoint_op = ***endpoint;
155 let (written, issues) = endpoint
156 .turbopack_ctx()
157 .turbo_tasks()
158 .run(async move {
159 let written_entrypoint_with_issues_op =
160 get_written_endpoint_with_issues_operation(endpoint_op);
161 let read = read_strongly_consistent_and_apply_effects(
162 written_entrypoint_with_issues_op,
163 |v| &v.effects,
164 )
165 .await?;
166 let WrittenEndpointWithIssues {
167 written, issues, ..
168 } = &*read;
169
170 Ok((written.clone(), issues.clone()))
171 })
172 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
173 .await?;
174 Ok(TurbopackResult {
175 result: NapiWrittenEndpoint::from(written.map(ReadRef::into_owned)),
176 issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
177 })
178}
179
180#[tracing::instrument(level = "info", name = "get server-side endpoint changes", skip_all)]
181#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
182pub fn endpoint_server_changed_subscribe(
183 #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
184 issues: bool,
185 func: JsFunction,
186) -> napi::Result<External<RootTask>> {
187 let turbopack_ctx = endpoint.turbopack_ctx().clone();
188 let endpoint = ***endpoint;
189 subscribe(
190 turbopack_ctx,
191 func,
192 move || {
193 async move {
194 let issues_and_diags_op = subscribe_issues_and_diags_operation(endpoint, issues);
195 let result =
196 read_strongly_consistent_and_apply_effects(issues_and_diags_op, |v| &v.effects)
197 .await?;
198 Ok(result)
199 }
200 .instrument(tracing::info_span!("server changes subscription"))
201 },
202 |ctx| {
203 let EndpointIssuesAndDiags {
204 changed: _,
205 issues,
206 effects: _,
207 } = &*ctx.value;
208
209 Ok(vec![TurbopackResult {
210 result: (),
211 issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
212 }])
213 },
214 )
215}
216
217#[turbo_tasks::value(shared, serialization = "skip", eq = "manual")]
218struct EndpointIssuesAndDiags {
219 changed: Option<ReadRef<Completion>>,
220 issues: Arc<Vec<ReadRef<PlainIssue>>>,
221 effects: Arc<Effects>,
222}
223
224impl PartialEq for EndpointIssuesAndDiags {
225 fn eq(&self, other: &Self) -> bool {
226 (match (&self.changed, &other.changed) {
227 (Some(a), Some(b)) => ReadRef::ptr_eq(a, b),
228 (None, None) => true,
229 (None, Some(_)) | (Some(_), None) => false,
230 }) && self.issues == other.issues
231 }
232}
233
234impl Eq for EndpointIssuesAndDiags {}
235
236#[turbo_tasks::function(operation, root)]
237async fn subscribe_issues_and_diags_operation(
238 endpoint_op: OperationVc<OptionEndpoint>,
239 should_include_issues: bool,
240) -> Result<Vc<EndpointIssuesAndDiags>> {
241 let changed_op = endpoint_server_changed_operation(endpoint_op);
242
243 let filter = issue_filter_from_endpoint(endpoint_op).await;
249 let (changed_value, issues, effects) =
250 strongly_consistent_catch_collectables(changed_op, &filter).await?;
251 Ok(EndpointIssuesAndDiags {
252 changed: changed_value,
253 issues: if should_include_issues {
254 issues
255 } else {
256 Arc::new(vec![])
257 },
258 effects,
259 }
260 .cell())
261}
262
263#[tracing::instrument(level = "info", name = "get client-side endpoint changes", skip_all)]
264#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
265pub fn endpoint_client_changed_subscribe(
266 #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
267 func: JsFunction,
268) -> napi::Result<External<RootTask>> {
269 let turbopack_ctx = endpoint.turbopack_ctx().clone();
270 let endpoint_op = ***endpoint;
271 subscribe(
272 turbopack_ctx,
273 func,
274 move || {
275 async move {
276 let changed_op = endpoint_client_changed_operation(endpoint_op);
277 let _ = changed_op.read_strongly_consistent().await?;
286 Ok(())
287 }
288 .instrument(tracing::info_span!("client changes subscription"))
289 },
290 |_| {
291 Ok(vec![TurbopackResult {
292 result: (),
293 issues: vec![],
294 }])
295 },
296 )
297}