Skip to main content

next_napi_bindings/next_api/
endpoint.rs

1use 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_tasks::{Completion, Effects, OperationVc, ReadRef, Vc};
17use turbopack_core::{
18    diagnostics::PlainDiagnostic,
19    issue::{IssueFilter, PlainIssue},
20};
21
22use crate::next_api::utils::{
23    DetachedVc, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult,
24    strongly_consistent_catch_collectables, 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: String,
35    pub content_hash: String,
36}
37
38impl From<AssetPath> for NapiAssetPath {
39    fn from(asset_path: AssetPath) -> Self {
40        Self {
41            path: asset_path.path.into_owned(),
42            content_hash: asset_path.content_hash.into_owned(),
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
88// NOTE(alexkirsz) We go through an extra layer of indirection here because of
89// two factors:
90// 1. rustc currently has a bug where using a dyn trait as a type argument to
91//    some async functions (in this case `endpoint_write_to_disk`) can cause
92//    higher-ranked lifetime errors. See https://github.com/rust-lang/rust/issues/102211
93// 2. the type_complexity clippy lint.
94pub 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
104/// Build an `IssueFilter` by reading the project from the endpoint's
105/// `OperationVc<OptionEndpoint>` and extracting ignore rules from its config.
106async fn issue_filter_from_endpoint(
107    endpoint_op: OperationVc<OptionEndpoint>,
108) -> Result<Vc<IssueFilter>> {
109    let endpoint_option = endpoint_op.connect().await?;
110    if let Some(ep) = &*endpoint_option {
111        Ok(ep.project().issue_filter())
112    } else {
113        Ok(IssueFilter::warnings_and_foreign_errors().cell())
114    }
115}
116
117#[turbo_tasks::value(serialization = "none")]
118struct WrittenEndpointWithIssues {
119    written: Option<ReadRef<EndpointOutputPaths>>,
120    issues: Arc<Vec<ReadRef<PlainIssue>>>,
121    diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
122    effects: Arc<Effects>,
123}
124
125#[turbo_tasks::function(operation)]
126async fn get_written_endpoint_with_issues_operation(
127    endpoint_op: OperationVc<OptionEndpoint>,
128) -> Result<Vc<WrittenEndpointWithIssues>> {
129    let write_to_disk_op = endpoint_write_to_disk_operation(endpoint_op);
130    let filter = issue_filter_from_endpoint(endpoint_op).await?;
131    let (written, issues, diagnostics, effects) =
132        strongly_consistent_catch_collectables(write_to_disk_op, filter).await?;
133    Ok(WrittenEndpointWithIssues {
134        written,
135        issues,
136        diagnostics,
137        effects,
138    }
139    .cell())
140}
141
142#[tracing::instrument(level = "info", name = "write endpoint to disk", skip_all)]
143#[napi]
144pub async fn endpoint_write_to_disk(
145    #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
146) -> napi::Result<TurbopackResult<NapiWrittenEndpoint>> {
147    let ctx = endpoint.turbopack_ctx();
148    let endpoint_op = ***endpoint;
149    let (written, issues, diags) = endpoint
150        .turbopack_ctx()
151        .turbo_tasks()
152        .run(async move {
153            let written_entrypoint_with_issues_op =
154                get_written_endpoint_with_issues_operation(endpoint_op);
155            let WrittenEndpointWithIssues {
156                written,
157                issues,
158                diagnostics,
159                effects,
160            } = &*written_entrypoint_with_issues_op
161                .read_strongly_consistent()
162                .await?;
163            effects.apply().await?;
164
165            Ok((written.clone(), issues.clone(), diagnostics.clone()))
166        })
167        .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
168        .await?;
169    Ok(TurbopackResult {
170        result: NapiWrittenEndpoint::from(written.map(ReadRef::into_owned)),
171        issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
172        diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(),
173    })
174}
175
176#[tracing::instrument(level = "info", name = "get server-side endpoint changes", skip_all)]
177#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
178pub fn endpoint_server_changed_subscribe(
179    #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
180    issues: bool,
181    func: JsFunction,
182) -> napi::Result<External<RootTask>> {
183    let turbopack_ctx = endpoint.turbopack_ctx().clone();
184    let endpoint = ***endpoint;
185    subscribe(
186        turbopack_ctx,
187        func,
188        move || {
189            async move {
190                let issues_and_diags_op = subscribe_issues_and_diags_operation(endpoint, issues);
191                let result = issues_and_diags_op.read_strongly_consistent().await?;
192                result.effects.apply().await?;
193                Ok(result)
194            }
195            .instrument(tracing::info_span!("server changes subscription"))
196        },
197        |ctx| {
198            let EndpointIssuesAndDiags {
199                changed: _,
200                issues,
201                diagnostics,
202                effects: _,
203            } = &*ctx.value;
204
205            Ok(vec![TurbopackResult {
206                result: (),
207                issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
208                diagnostics: diagnostics
209                    .iter()
210                    .map(|d| NapiDiagnostic::from(d))
211                    .collect(),
212            }])
213        },
214    )
215}
216
217#[turbo_tasks::value(shared, serialization = "none", eq = "manual")]
218struct EndpointIssuesAndDiags {
219    changed: Option<ReadRef<Completion>>,
220    issues: Arc<Vec<ReadRef<PlainIssue>>>,
221    diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
222    effects: Arc<Effects>,
223}
224
225impl PartialEq for EndpointIssuesAndDiags {
226    fn eq(&self, other: &Self) -> bool {
227        (match (&self.changed, &other.changed) {
228            (Some(a), Some(b)) => ReadRef::ptr_eq(a, b),
229            (None, None) => true,
230            (None, Some(_)) | (Some(_), None) => false,
231        }) && self.issues == other.issues
232            && self.diagnostics == other.diagnostics
233    }
234}
235
236impl Eq for EndpointIssuesAndDiags {}
237
238#[turbo_tasks::function(operation)]
239async fn subscribe_issues_and_diags_operation(
240    endpoint_op: OperationVc<OptionEndpoint>,
241    should_include_issues: bool,
242) -> Result<Vc<EndpointIssuesAndDiags>> {
243    let changed_op = endpoint_server_changed_operation(endpoint_op);
244
245    if should_include_issues {
246        let filter = issue_filter_from_endpoint(endpoint_op).await?;
247        let (changed_value, issues, diagnostics, effects) =
248            strongly_consistent_catch_collectables(changed_op, filter).await?;
249        Ok(EndpointIssuesAndDiags {
250            changed: changed_value,
251            issues,
252            diagnostics,
253            effects,
254        }
255        .cell())
256    } else {
257        let changed_value = changed_op.read_strongly_consistent().await?;
258        Ok(EndpointIssuesAndDiags {
259            changed: Some(changed_value),
260            issues: Arc::new(vec![]),
261            diagnostics: Arc::new(vec![]),
262            effects: Arc::new(Effects::default()),
263        }
264        .cell())
265    }
266}
267
268#[tracing::instrument(level = "info", name = "get client-side endpoint changes", skip_all)]
269#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
270pub fn endpoint_client_changed_subscribe(
271    #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
272    func: JsFunction,
273) -> napi::Result<External<RootTask>> {
274    let turbopack_ctx = endpoint.turbopack_ctx().clone();
275    let endpoint_op = ***endpoint;
276    subscribe(
277        turbopack_ctx,
278        func,
279        move || {
280            async move {
281                let changed_op = endpoint_client_changed_operation(endpoint_op);
282                // We don't capture issues and diagnostics here since we don't want to be
283                // notified when they change
284                //
285                // This must be a *read*, not just a resolve, because we need the root task created
286                // by `subscribe` to re-run when the `Completion`'s value changes (via equality),
287                // even if the cell id doesn't change.
288                let _ = changed_op.read_strongly_consistent().await?;
289                Ok(())
290            }
291            .instrument(tracing::info_span!("client changes subscription"))
292        },
293        |_| {
294            Ok(vec![TurbopackResult {
295                result: (),
296                issues: vec![],
297                diagnostics: vec![],
298            }])
299        },
300    )
301}