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
83pub 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 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}