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