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