next_swc_napi/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::ServerPath,
10    route::{
11        EndpointOutputPaths, endpoint_client_changed_operation, endpoint_server_changed_operation,
12        endpoint_write_to_disk_operation,
13    },
14};
15use tracing::Instrument;
16use turbo_tasks::{Completion, Effects, OperationVc, ReadRef, Vc};
17use turbopack_core::{diagnostics::PlainDiagnostic, issue::PlainIssue};
18
19use crate::next_api::utils::{
20    DetachedVc, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult,
21    strongly_consistent_catch_collectables, subscribe,
22};
23
24#[napi(object)]
25#[derive(Default)]
26pub struct NapiEndpointConfig {}
27
28#[napi(object)]
29#[derive(Default)]
30pub struct NapiServerPath {
31    pub path: String,
32    pub content_hash: String,
33}
34
35impl From<ServerPath> for NapiServerPath {
36    fn from(server_path: ServerPath) -> Self {
37        Self {
38            path: server_path.path.into_owned(),
39            content_hash: format!("{:x}", server_path.content_hash),
40        }
41    }
42}
43
44#[napi(object)]
45#[derive(Default)]
46pub struct NapiWrittenEndpoint {
47    pub r#type: String,
48    pub entry_path: Option<String>,
49    pub client_paths: Vec<String>,
50    pub server_paths: Vec<NapiServerPath>,
51    pub config: NapiEndpointConfig,
52}
53
54impl From<Option<EndpointOutputPaths>> for NapiWrittenEndpoint {
55    fn from(written_endpoint: Option<EndpointOutputPaths>) -> Self {
56        match written_endpoint {
57            Some(EndpointOutputPaths::NodeJs {
58                server_entry_path,
59                server_paths,
60                client_paths,
61            }) => Self {
62                r#type: "nodejs".to_string(),
63                entry_path: Some(server_entry_path.into_owned()),
64                client_paths: client_paths.into_iter().map(From::from).collect(),
65                server_paths: server_paths.into_iter().map(From::from).collect(),
66                ..Default::default()
67            },
68            Some(EndpointOutputPaths::Edge {
69                server_paths,
70                client_paths,
71            }) => Self {
72                r#type: "edge".to_string(),
73                client_paths: client_paths.into_iter().map(From::from).collect(),
74                server_paths: server_paths.into_iter().map(From::from).collect(),
75                ..Default::default()
76            },
77            Some(EndpointOutputPaths::NotFound) | None => Self {
78                r#type: "none".to_string(),
79                ..Default::default()
80            },
81        }
82    }
83}
84
85// NOTE(alexkirsz) We go through an extra layer of indirection here because of
86// two factors:
87// 1. rustc currently has a bug where using a dyn trait as a type argument to
88//    some async functions (in this case `endpoint_write_to_disk`) can cause
89//    higher-ranked lifetime errors. See https://github.com/rust-lang/rust/issues/102211
90// 2. the type_complexity clippy lint.
91pub struct ExternalEndpoint(pub DetachedVc<OptionEndpoint>);
92
93impl Deref for ExternalEndpoint {
94    type Target = DetachedVc<OptionEndpoint>;
95
96    fn deref(&self) -> &Self::Target {
97        &self.0
98    }
99}
100
101#[turbo_tasks::value(serialization = "none")]
102struct WrittenEndpointWithIssues {
103    written: Option<ReadRef<EndpointOutputPaths>>,
104    issues: Arc<Vec<ReadRef<PlainIssue>>>,
105    diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
106    effects: Arc<Effects>,
107}
108
109#[turbo_tasks::function(operation)]
110async fn get_written_endpoint_with_issues_operation(
111    endpoint_op: OperationVc<OptionEndpoint>,
112) -> Result<Vc<WrittenEndpointWithIssues>> {
113    let write_to_disk_op = endpoint_write_to_disk_operation(endpoint_op);
114    let (written, issues, diagnostics, effects) =
115        strongly_consistent_catch_collectables(write_to_disk_op).await?;
116    Ok(WrittenEndpointWithIssues {
117        written,
118        issues,
119        diagnostics,
120        effects,
121    }
122    .cell())
123}
124
125#[tracing::instrument(level = "info", name = "write endpoint to disk", skip_all)]
126#[napi]
127pub async fn endpoint_write_to_disk(
128    #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
129) -> napi::Result<TurbopackResult<NapiWrittenEndpoint>> {
130    let ctx = endpoint.turbopack_ctx();
131    let endpoint_op = ***endpoint;
132    let (written, issues, diags) = endpoint
133        .turbopack_ctx()
134        .turbo_tasks()
135        .run(async move {
136            let written_entrypoint_with_issues_op =
137                get_written_endpoint_with_issues_operation(endpoint_op);
138            let WrittenEndpointWithIssues {
139                written,
140                issues,
141                diagnostics,
142                effects,
143            } = &*written_entrypoint_with_issues_op
144                .read_strongly_consistent()
145                .await?;
146            effects.apply().await?;
147
148            Ok((written.clone(), issues.clone(), diagnostics.clone()))
149        })
150        .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
151        .await?;
152    Ok(TurbopackResult {
153        result: NapiWrittenEndpoint::from(written.map(ReadRef::into_owned)),
154        issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
155        diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(),
156    })
157}
158
159#[tracing::instrument(level = "info", name = "get server-side endpoint changes", skip_all)]
160#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
161pub fn endpoint_server_changed_subscribe(
162    #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
163    issues: bool,
164    func: JsFunction,
165) -> napi::Result<External<RootTask>> {
166    let turbopack_ctx = endpoint.turbopack_ctx().clone();
167    let endpoint = ***endpoint;
168    subscribe(
169        turbopack_ctx,
170        func,
171        move || {
172            async move {
173                let issues_and_diags_op = subscribe_issues_and_diags_operation(endpoint, issues);
174                let result = issues_and_diags_op.read_strongly_consistent().await?;
175                result.effects.apply().await?;
176                Ok(result)
177            }
178            .instrument(tracing::info_span!("server changes subscription"))
179        },
180        |ctx| {
181            let EndpointIssuesAndDiags {
182                changed: _,
183                issues,
184                diagnostics,
185                effects: _,
186            } = &*ctx.value;
187
188            Ok(vec![TurbopackResult {
189                result: (),
190                issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
191                diagnostics: diagnostics
192                    .iter()
193                    .map(|d| NapiDiagnostic::from(d))
194                    .collect(),
195            }])
196        },
197    )
198}
199
200#[turbo_tasks::value(shared, serialization = "none", eq = "manual")]
201struct EndpointIssuesAndDiags {
202    changed: Option<ReadRef<Completion>>,
203    issues: Arc<Vec<ReadRef<PlainIssue>>>,
204    diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
205    effects: Arc<Effects>,
206}
207
208impl PartialEq for EndpointIssuesAndDiags {
209    fn eq(&self, other: &Self) -> bool {
210        (match (&self.changed, &other.changed) {
211            (Some(a), Some(b)) => ReadRef::ptr_eq(a, b),
212            (None, None) => true,
213            (None, Some(_)) | (Some(_), None) => false,
214        }) && self.issues == other.issues
215            && self.diagnostics == other.diagnostics
216    }
217}
218
219impl Eq for EndpointIssuesAndDiags {}
220
221#[turbo_tasks::function(operation)]
222async fn subscribe_issues_and_diags_operation(
223    endpoint_op: OperationVc<OptionEndpoint>,
224    should_include_issues: bool,
225) -> Result<Vc<EndpointIssuesAndDiags>> {
226    let changed_op = endpoint_server_changed_operation(endpoint_op);
227
228    if should_include_issues {
229        let (changed_value, issues, diagnostics, effects) =
230            strongly_consistent_catch_collectables(changed_op).await?;
231        Ok(EndpointIssuesAndDiags {
232            changed: changed_value,
233            issues,
234            diagnostics,
235            effects,
236        }
237        .cell())
238    } else {
239        let changed_value = changed_op.read_strongly_consistent().await?;
240        Ok(EndpointIssuesAndDiags {
241            changed: Some(changed_value),
242            issues: Arc::new(vec![]),
243            diagnostics: Arc::new(vec![]),
244            effects: Arc::new(Effects::default()),
245        }
246        .cell())
247    }
248}
249
250#[tracing::instrument(level = "info", name = "get client-side endpoint changes", skip_all)]
251#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
252pub fn endpoint_client_changed_subscribe(
253    #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
254    func: JsFunction,
255) -> napi::Result<External<RootTask>> {
256    let turbopack_ctx = endpoint.turbopack_ctx().clone();
257    let endpoint_op = ***endpoint;
258    subscribe(
259        turbopack_ctx,
260        func,
261        move || {
262            async move {
263                let changed_op = endpoint_client_changed_operation(endpoint_op);
264                // We don't capture issues and diagnostics here since we don't want to be
265                // notified when they change
266                //
267                // This must be a *read*, not just a resolve, because we need the root task created
268                // by `subscribe` to re-run when the `Completion`'s value changes (via equality),
269                // even if the cell id doesn't change.
270                let _ = changed_op.read_strongly_consistent().await?;
271                Ok(())
272            }
273            .instrument(tracing::info_span!("client changes subscription"))
274        },
275        |_| {
276            Ok(vec![TurbopackResult {
277                result: (),
278                issues: vec![],
279                diagnostics: vec![],
280            }])
281        },
282    )
283}