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#[napi]
125#[tracing::instrument(skip_all)]
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_once(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))
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#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
159pub fn endpoint_server_changed_subscribe(
160    #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
161    issues: bool,
162    func: JsFunction,
163) -> napi::Result<External<RootTask>> {
164    let turbopack_ctx = endpoint.turbopack_ctx().clone();
165    let endpoint = ***endpoint;
166    subscribe(
167        turbopack_ctx,
168        func,
169        move || {
170            async move {
171                let issues_and_diags_op = subscribe_issues_and_diags_operation(endpoint, issues);
172                let result = issues_and_diags_op.read_strongly_consistent().await?;
173                result.effects.apply().await?;
174                Ok(result)
175            }
176            .instrument(tracing::info_span!("server changes subscription"))
177        },
178        |ctx| {
179            let EndpointIssuesAndDiags {
180                changed: _,
181                issues,
182                diagnostics,
183                effects: _,
184            } = &*ctx.value;
185
186            Ok(vec![TurbopackResult {
187                result: (),
188                issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
189                diagnostics: diagnostics
190                    .iter()
191                    .map(|d| NapiDiagnostic::from(d))
192                    .collect(),
193            }])
194        },
195    )
196}
197
198#[turbo_tasks::value(shared, serialization = "none", eq = "manual")]
199struct EndpointIssuesAndDiags {
200    changed: Option<ReadRef<Completion>>,
201    issues: Arc<Vec<ReadRef<PlainIssue>>>,
202    diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
203    effects: Arc<Effects>,
204}
205
206impl PartialEq for EndpointIssuesAndDiags {
207    fn eq(&self, other: &Self) -> bool {
208        (match (&self.changed, &other.changed) {
209            (Some(a), Some(b)) => ReadRef::ptr_eq(a, b),
210            (None, None) => true,
211            (None, Some(_)) | (Some(_), None) => false,
212        }) && self.issues == other.issues
213            && self.diagnostics == other.diagnostics
214    }
215}
216
217impl Eq for EndpointIssuesAndDiags {}
218
219#[turbo_tasks::function(operation)]
220async fn subscribe_issues_and_diags_operation(
221    endpoint_op: OperationVc<OptionEndpoint>,
222    should_include_issues: bool,
223) -> Result<Vc<EndpointIssuesAndDiags>> {
224    let changed_op = endpoint_server_changed_operation(endpoint_op);
225
226    if should_include_issues {
227        let (changed_value, issues, diagnostics, effects) =
228            strongly_consistent_catch_collectables(changed_op).await?;
229        Ok(EndpointIssuesAndDiags {
230            changed: changed_value,
231            issues,
232            diagnostics,
233            effects,
234        }
235        .cell())
236    } else {
237        let changed_value = changed_op.read_strongly_consistent().await?;
238        Ok(EndpointIssuesAndDiags {
239            changed: Some(changed_value),
240            issues: Arc::new(vec![]),
241            diagnostics: Arc::new(vec![]),
242            effects: Arc::new(Effects::default()),
243        }
244        .cell())
245    }
246}
247
248#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
249pub fn endpoint_client_changed_subscribe(
250    #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
251    func: JsFunction,
252) -> napi::Result<External<RootTask>> {
253    let turbopack_ctx = endpoint.turbopack_ctx().clone();
254    let endpoint_op = ***endpoint;
255    subscribe(
256        turbopack_ctx,
257        func,
258        move || {
259            async move {
260                let changed_op = endpoint_client_changed_operation(endpoint_op);
261                // We don't capture issues and diagnostics here since we don't want to be
262                // notified when they change
263                //
264                // This must be a *read*, not just a resolve, because we need the root task created
265                // by `subscribe` to re-run when the `Completion`'s value changes (via equality),
266                // even if the cell id doesn't change.
267                let _ = changed_op.read_strongly_consistent().await?;
268                Ok(())
269            }
270            .instrument(tracing::info_span!("client changes subscription"))
271        },
272        |_| {
273            Ok(vec![TurbopackResult {
274                result: (),
275                issues: vec![],
276                diagnostics: vec![],
277            }])
278        },
279    )
280}