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