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