1use std::{ops::Deref, sync::Arc};
2
3use anyhow::Result;
4use futures_util::TryFutureExt;
5use napi::{JsFunction, bindgen_prelude::External};
6use napi_derive::napi;
7use next_api::{
8 operation::OptionEndpoint,
9 paths::ServerPath,
10 route::{
11 EndpointOutputPaths, endpoint_client_changed_operation, endpoint_server_changed_operation,
12 endpoint_write_to_disk_operation,
13 },
14};
15use tracing::Instrument;
16use turbo_tasks::{Completion, Effects, OperationVc, ReadRef, Vc};
17use turbopack_core::{diagnostics::PlainDiagnostic, issue::PlainIssue};
18
19use crate::next_api::utils::{
20 DetachedVc, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult,
21 strongly_consistent_catch_collectables, subscribe,
22};
23
24#[napi(object)]
25#[derive(Default)]
26pub struct NapiEndpointConfig {}
27
28#[napi(object)]
29#[derive(Default)]
30pub struct NapiServerPath {
31 pub path: String,
32 pub content_hash: String,
33}
34
35impl From<ServerPath> for NapiServerPath {
36 fn from(server_path: ServerPath) -> Self {
37 Self {
38 path: server_path.path.into_owned(),
39 content_hash: format!("{:x}", server_path.content_hash),
40 }
41 }
42}
43
44#[napi(object)]
45#[derive(Default)]
46pub struct NapiWrittenEndpoint {
47 pub r#type: String,
48 pub entry_path: Option<String>,
49 pub client_paths: Vec<String>,
50 pub server_paths: Vec<NapiServerPath>,
51 pub config: NapiEndpointConfig,
52}
53
54impl From<Option<EndpointOutputPaths>> for NapiWrittenEndpoint {
55 fn from(written_endpoint: Option<EndpointOutputPaths>) -> Self {
56 match written_endpoint {
57 Some(EndpointOutputPaths::NodeJs {
58 server_entry_path,
59 server_paths,
60 client_paths,
61 }) => Self {
62 r#type: "nodejs".to_string(),
63 entry_path: Some(server_entry_path.into_owned()),
64 client_paths: client_paths.into_iter().map(From::from).collect(),
65 server_paths: server_paths.into_iter().map(From::from).collect(),
66 ..Default::default()
67 },
68 Some(EndpointOutputPaths::Edge {
69 server_paths,
70 client_paths,
71 }) => Self {
72 r#type: "edge".to_string(),
73 client_paths: client_paths.into_iter().map(From::from).collect(),
74 server_paths: server_paths.into_iter().map(From::from).collect(),
75 ..Default::default()
76 },
77 Some(EndpointOutputPaths::NotFound) | None => Self {
78 r#type: "none".to_string(),
79 ..Default::default()
80 },
81 }
82 }
83}
84
85pub struct ExternalEndpoint(pub DetachedVc<OptionEndpoint>);
92
93impl Deref for ExternalEndpoint {
94 type Target = DetachedVc<OptionEndpoint>;
95
96 fn deref(&self) -> &Self::Target {
97 &self.0
98 }
99}
100
101#[turbo_tasks::value(serialization = "none")]
102struct WrittenEndpointWithIssues {
103 written: Option<ReadRef<EndpointOutputPaths>>,
104 issues: Arc<Vec<ReadRef<PlainIssue>>>,
105 diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
106 effects: Arc<Effects>,
107}
108
109#[turbo_tasks::function(operation)]
110async fn get_written_endpoint_with_issues_operation(
111 endpoint_op: OperationVc<OptionEndpoint>,
112) -> Result<Vc<WrittenEndpointWithIssues>> {
113 let write_to_disk_op = endpoint_write_to_disk_operation(endpoint_op);
114 let (written, issues, diagnostics, effects) =
115 strongly_consistent_catch_collectables(write_to_disk_op).await?;
116 Ok(WrittenEndpointWithIssues {
117 written,
118 issues,
119 diagnostics,
120 effects,
121 }
122 .cell())
123}
124
125#[tracing::instrument(level = "info", name = "write endpoint to disk", skip_all)]
126#[napi]
127pub async fn endpoint_write_to_disk(
128 #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
129) -> napi::Result<TurbopackResult<NapiWrittenEndpoint>> {
130 let ctx = endpoint.turbopack_ctx();
131 let endpoint_op = ***endpoint;
132 let (written, issues, diags) = endpoint
133 .turbopack_ctx()
134 .turbo_tasks()
135 .run(async move {
136 let written_entrypoint_with_issues_op =
137 get_written_endpoint_with_issues_operation(endpoint_op);
138 let WrittenEndpointWithIssues {
139 written,
140 issues,
141 diagnostics,
142 effects,
143 } = &*written_entrypoint_with_issues_op
144 .read_strongly_consistent()
145 .await?;
146 effects.apply().await?;
147
148 Ok((written.clone(), issues.clone(), diagnostics.clone()))
149 })
150 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
151 .await?;
152 Ok(TurbopackResult {
153 result: NapiWrittenEndpoint::from(written.map(ReadRef::into_owned)),
154 issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
155 diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(),
156 })
157}
158
159#[tracing::instrument(level = "info", name = "get server-side endpoint changes", skip_all)]
160#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
161pub fn endpoint_server_changed_subscribe(
162 #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
163 issues: bool,
164 func: JsFunction,
165) -> napi::Result<External<RootTask>> {
166 let turbopack_ctx = endpoint.turbopack_ctx().clone();
167 let endpoint = ***endpoint;
168 subscribe(
169 turbopack_ctx,
170 func,
171 move || {
172 async move {
173 let issues_and_diags_op = subscribe_issues_and_diags_operation(endpoint, issues);
174 let result = issues_and_diags_op.read_strongly_consistent().await?;
175 result.effects.apply().await?;
176 Ok(result)
177 }
178 .instrument(tracing::info_span!("server changes subscription"))
179 },
180 |ctx| {
181 let EndpointIssuesAndDiags {
182 changed: _,
183 issues,
184 diagnostics,
185 effects: _,
186 } = &*ctx.value;
187
188 Ok(vec![TurbopackResult {
189 result: (),
190 issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
191 diagnostics: diagnostics
192 .iter()
193 .map(|d| NapiDiagnostic::from(d))
194 .collect(),
195 }])
196 },
197 )
198}
199
200#[turbo_tasks::value(shared, serialization = "none", eq = "manual")]
201struct EndpointIssuesAndDiags {
202 changed: Option<ReadRef<Completion>>,
203 issues: Arc<Vec<ReadRef<PlainIssue>>>,
204 diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
205 effects: Arc<Effects>,
206}
207
208impl PartialEq for EndpointIssuesAndDiags {
209 fn eq(&self, other: &Self) -> bool {
210 (match (&self.changed, &other.changed) {
211 (Some(a), Some(b)) => ReadRef::ptr_eq(a, b),
212 (None, None) => true,
213 (None, Some(_)) | (Some(_), None) => false,
214 }) && self.issues == other.issues
215 && self.diagnostics == other.diagnostics
216 }
217}
218
219impl Eq for EndpointIssuesAndDiags {}
220
221#[turbo_tasks::function(operation)]
222async fn subscribe_issues_and_diags_operation(
223 endpoint_op: OperationVc<OptionEndpoint>,
224 should_include_issues: bool,
225) -> Result<Vc<EndpointIssuesAndDiags>> {
226 let changed_op = endpoint_server_changed_operation(endpoint_op);
227
228 if should_include_issues {
229 let (changed_value, issues, diagnostics, effects) =
230 strongly_consistent_catch_collectables(changed_op).await?;
231 Ok(EndpointIssuesAndDiags {
232 changed: changed_value,
233 issues,
234 diagnostics,
235 effects,
236 }
237 .cell())
238 } else {
239 let changed_value = changed_op.read_strongly_consistent().await?;
240 Ok(EndpointIssuesAndDiags {
241 changed: Some(changed_value),
242 issues: Arc::new(vec![]),
243 diagnostics: Arc::new(vec![]),
244 effects: Arc::new(Effects::default()),
245 }
246 .cell())
247 }
248}
249
250#[tracing::instrument(level = "info", name = "get client-side endpoint changes", skip_all)]
251#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
252pub fn endpoint_client_changed_subscribe(
253 #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External<ExternalEndpoint>,
254 func: JsFunction,
255) -> napi::Result<External<RootTask>> {
256 let turbopack_ctx = endpoint.turbopack_ctx().clone();
257 let endpoint_op = ***endpoint;
258 subscribe(
259 turbopack_ctx,
260 func,
261 move || {
262 async move {
263 let changed_op = endpoint_client_changed_operation(endpoint_op);
264 let _ = changed_op.read_strongly_consistent().await?;
271 Ok(())
272 }
273 .instrument(tracing::info_span!("client changes subscription"))
274 },
275 |_| {
276 Ok(vec![TurbopackResult {
277 result: (),
278 issues: vec![],
279 diagnostics: vec![],
280 }])
281 },
282 )
283}