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_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
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.
106///
107/// If the upstream endpoint operation fails to resolve (e.g. because the build
108/// graph cannot be evaluated transiently — for example during a mid-session
109/// `node_modules` reshuffle), this falls back to a default filter rather than
110/// propagating the error.  In this scenario we believe the caller will already be observing the
111/// same error
112async 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    // Use catch-collectables in both branches so transient build-graph errors
244    // (e.g. missing `node_modules/next` during a concurrent install) surface as
245    // Issues rather than killing the subscription with a `TurbopackInternalError`.
246    // When `should_include_issues` is false the caller doesn't need the Issue
247    // payload, but we still need the catch path to avoid the FATAL.
248    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                // We don't capture issues and diagnostics here since we don't want to be
278                // notified when they change.  We also want errors to propagate so we don't use
279                // strongly_consistent_catch_collectibles either.
280                //
281                // This must be a *read*, not just a resolve, because we need the root task created
282                // by `subscribe` to re-run when the `Completion`'s value changes (via equality),
283                // even if the cell id doesn't change.
284                //
285                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}