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