next_swc_napi/next_api/
endpoint.rs

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