1use std::{future::Future, ops::Deref, sync::Arc};
2
3use anyhow::{Context, Result, anyhow};
4use napi::{
5 JsFunction, JsObject, JsUnknown, NapiRaw, NapiValue, Status,
6 bindgen_prelude::{External, ToNapiValue},
7 threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode},
8};
9use rustc_hash::FxHashMap;
10use serde::Serialize;
11use turbo_tasks::{
12 Effects, OperationVc, ReadRef, TaskId, TryJoinIterExt, Vc, VcValueType, get_effects,
13};
14use turbo_tasks_fs::FileContent;
15use turbopack_core::{
16 diagnostics::{Diagnostic, DiagnosticContextExt, PlainDiagnostic},
17 error::PrettyPrintError,
18 issue::{
19 IssueDescriptionExt, IssueSeverity, PlainIssue, PlainIssueSource, PlainSource, StyledString,
20 },
21 source_pos::SourcePos,
22};
23
24use crate::{next_api::turbopack_ctx::NextTurbopackContext, util::log_internal_error_and_inform};
25
26#[derive(Clone)]
38pub struct DetachedVc<T> {
39 turbopack_ctx: NextTurbopackContext,
40 vc: OperationVc<T>,
42}
43
44impl<T> DetachedVc<T> {
45 pub fn new(turbopack_ctx: NextTurbopackContext, vc: OperationVc<T>) -> Self {
46 Self { turbopack_ctx, vc }
47 }
48
49 pub fn turbopack_ctx(&self) -> &NextTurbopackContext {
50 &self.turbopack_ctx
51 }
52}
53
54impl<T> Deref for DetachedVc<T> {
55 type Target = OperationVc<T>;
56
57 fn deref(&self) -> &Self::Target {
58 &self.vc
59 }
60}
61
62pub fn serde_enum_to_string<T: Serialize>(value: &T) -> Result<String> {
63 Ok(serde_json::to_value(value)?
64 .as_str()
65 .context("value must serialize to a string")?
66 .to_string())
67}
68
69pub struct RootTask {
80 turbopack_ctx: NextTurbopackContext,
81 task_id: Option<TaskId>,
82}
83
84impl Drop for RootTask {
85 fn drop(&mut self) {
86 }
88}
89
90#[napi]
91pub fn root_task_dispose(
92 #[napi(ts_arg_type = "{ __napiType: \"RootTask\" }")] mut root_task: External<RootTask>,
93) -> napi::Result<()> {
94 if let Some(task) = root_task.task_id.take() {
95 root_task
96 .turbopack_ctx
97 .turbo_tasks()
98 .dispose_root_task(task);
99 }
100 Ok(())
101}
102
103pub async fn get_issues<T: Send>(source: OperationVc<T>) -> Result<Arc<Vec<ReadRef<PlainIssue>>>> {
104 let issues = source.peek_issues_with_path().await?;
105 Ok(Arc::new(issues.get_plain_issues().await?))
106}
107
108pub async fn get_diagnostics<T: Send>(
113 source: OperationVc<T>,
114) -> Result<Arc<Vec<ReadRef<PlainDiagnostic>>>> {
115 let captured_diags = source.peek_diagnostics().await?;
116 let mut diags = captured_diags
117 .diagnostics
118 .iter()
119 .map(|d| d.into_plain())
120 .try_join()
121 .await?;
122
123 diags.sort();
124
125 Ok(Arc::new(diags))
126}
127
128#[napi(object)]
129pub struct NapiIssue {
130 pub severity: String,
131 pub stage: String,
132 pub file_path: String,
133 pub title: serde_json::Value,
134 pub description: Option<serde_json::Value>,
135 pub detail: Option<serde_json::Value>,
136 pub source: Option<NapiIssueSource>,
137 pub documentation_link: String,
138 pub import_traces: serde_json::Value,
139}
140
141impl From<&PlainIssue> for NapiIssue {
142 fn from(issue: &PlainIssue) -> Self {
143 Self {
144 description: issue
145 .description
146 .as_ref()
147 .map(|styled| serde_json::to_value(StyledStringSerialize::from(styled)).unwrap()),
148 stage: issue.stage.to_string(),
149 file_path: issue.file_path.to_string(),
150 detail: issue
151 .detail
152 .as_ref()
153 .map(|styled| serde_json::to_value(StyledStringSerialize::from(styled)).unwrap()),
154 documentation_link: issue.documentation_link.to_string(),
155 severity: issue.severity.as_str().to_string(),
156 source: issue.source.as_ref().map(|source| source.into()),
157 title: serde_json::to_value(StyledStringSerialize::from(&issue.title)).unwrap(),
158 import_traces: serde_json::to_value(&issue.import_traces).unwrap(),
159 }
160 }
161}
162
163#[derive(Serialize)]
164#[serde(tag = "type", rename_all = "camelCase")]
165pub enum StyledStringSerialize<'a> {
166 Line {
167 value: Vec<StyledStringSerialize<'a>>,
168 },
169 Stack {
170 value: Vec<StyledStringSerialize<'a>>,
171 },
172 Text {
173 value: &'a str,
174 },
175 Code {
176 value: &'a str,
177 },
178 Strong {
179 value: &'a str,
180 },
181}
182
183impl<'a> From<&'a StyledString> for StyledStringSerialize<'a> {
184 fn from(value: &'a StyledString) -> Self {
185 match value {
186 StyledString::Line(parts) => StyledStringSerialize::Line {
187 value: parts.iter().map(|p| p.into()).collect(),
188 },
189 StyledString::Stack(parts) => StyledStringSerialize::Stack {
190 value: parts.iter().map(|p| p.into()).collect(),
191 },
192 StyledString::Text(string) => StyledStringSerialize::Text { value: string },
193 StyledString::Code(string) => StyledStringSerialize::Code { value: string },
194 StyledString::Strong(string) => StyledStringSerialize::Strong { value: string },
195 }
196 }
197}
198
199#[napi(object)]
200pub struct NapiIssueSource {
201 pub source: NapiSource,
202 pub range: Option<NapiIssueSourceRange>,
203}
204
205impl From<&PlainIssueSource> for NapiIssueSource {
206 fn from(
207 PlainIssueSource {
208 asset: source,
209 range,
210 }: &PlainIssueSource,
211 ) -> Self {
212 Self {
213 source: (&**source).into(),
214 range: range.as_ref().map(|range| range.into()),
215 }
216 }
217}
218
219#[napi(object)]
220pub struct NapiIssueSourceRange {
221 pub start: NapiSourcePos,
222 pub end: NapiSourcePos,
223}
224
225impl From<&(SourcePos, SourcePos)> for NapiIssueSourceRange {
226 fn from((start, end): &(SourcePos, SourcePos)) -> Self {
227 Self {
228 start: (*start).into(),
229 end: (*end).into(),
230 }
231 }
232}
233
234#[napi(object)]
235pub struct NapiSource {
236 pub ident: String,
237 pub content: Option<String>,
238}
239
240impl From<&PlainSource> for NapiSource {
241 fn from(source: &PlainSource) -> Self {
242 Self {
243 ident: source.ident.to_string(),
244 content: match &*source.content {
245 FileContent::Content(content) => match content.content().to_str() {
246 Ok(str) => Some(str.into_owned()),
247 Err(_) => None,
248 },
249 FileContent::NotFound => None,
250 },
251 }
252 }
253}
254
255#[napi(object)]
256pub struct NapiSourcePos {
257 pub line: u32,
258 pub column: u32,
259}
260
261impl From<SourcePos> for NapiSourcePos {
262 fn from(pos: SourcePos) -> Self {
263 Self {
264 line: pos.line,
265 column: pos.column,
266 }
267 }
268}
269
270#[napi(object)]
271pub struct NapiDiagnostic {
272 pub category: String,
273 pub name: String,
274 #[napi(ts_type = "Record<string, string>")]
275 pub payload: FxHashMap<String, String>,
276}
277
278impl NapiDiagnostic {
279 pub fn from(diagnostic: &PlainDiagnostic) -> Self {
280 Self {
281 category: diagnostic.category.to_string(),
282 name: diagnostic.name.to_string(),
283 payload: diagnostic
284 .payload
285 .iter()
286 .map(|(k, v)| (k.to_string(), v.to_string()))
287 .collect(),
288 }
289 }
290}
291
292pub struct TurbopackResult<T: ToNapiValue> {
293 pub result: T,
294 pub issues: Vec<NapiIssue>,
295 pub diagnostics: Vec<NapiDiagnostic>,
296}
297
298impl<T: ToNapiValue> ToNapiValue for TurbopackResult<T> {
299 unsafe fn to_napi_value(
300 env: napi::sys::napi_env,
301 val: Self,
302 ) -> napi::Result<napi::sys::napi_value> {
303 let mut obj = unsafe { napi::Env::from_raw(env).create_object()? };
304
305 let result = unsafe {
306 let result = T::to_napi_value(env, val.result)?;
307 JsUnknown::from_raw(env, result)?
308 };
309 if matches!(result.get_type()?, napi::ValueType::Object) {
310 let result = unsafe { result.cast::<JsObject>() };
312
313 for key in JsObject::keys(&result)? {
314 let value: JsUnknown = result.get_named_property(&key)?;
315 obj.set_named_property(&key, value)?;
316 }
317 }
318
319 obj.set_named_property("issues", val.issues)?;
320 obj.set_named_property("diagnostics", val.diagnostics)?;
321
322 Ok(unsafe { obj.raw() })
323 }
324}
325
326pub fn subscribe<T: 'static + Send + Sync, F: Future<Output = Result<T>> + Send, V: ToNapiValue>(
327 turbopack_ctx: NextTurbopackContext,
328 func: JsFunction,
329 handler: impl 'static + Sync + Send + Clone + Fn() -> F,
330 mapper: impl 'static + Sync + Send + FnMut(ThreadSafeCallContext<T>) -> napi::Result<Vec<V>>,
331) -> napi::Result<External<RootTask>> {
332 let func: ThreadsafeFunction<T> = func.create_threadsafe_function(0, mapper)?;
333 let task_id = turbopack_ctx.turbo_tasks().spawn_root_task(move || {
334 let handler = handler.clone();
335 let func = func.clone();
336 Box::pin(async move {
337 let result = handler().await;
338
339 let status = func.call(
340 result.map_err(|e| {
341 log_internal_error_and_inform(&e);
342 napi::Error::from_reason(PrettyPrintError(&e).to_string())
343 }),
344 ThreadsafeFunctionCallMode::NonBlocking,
345 );
346 if !matches!(status, Status::Ok) {
347 let error = anyhow!("Error calling JS function: {}", status);
348 eprintln!("{error}");
349 return Err::<Vc<()>, _>(error);
350 }
351 Ok(Default::default())
352 })
353 });
354 Ok(External::new(RootTask {
355 turbopack_ctx,
356 task_id: Some(task_id),
357 }))
358}
359
360pub async fn strongly_consistent_catch_collectables<R: VcValueType + Send>(
363 source_op: OperationVc<R>,
364) -> Result<(
365 Option<ReadRef<R>>,
366 Arc<Vec<ReadRef<PlainIssue>>>,
367 Arc<Vec<ReadRef<PlainDiagnostic>>>,
368 Arc<Effects>,
369)> {
370 let result = source_op.read_strongly_consistent().await;
371 let issues = get_issues(source_op).await?;
372 let diagnostics = get_diagnostics(source_op).await?;
373 let effects = Arc::new(get_effects(source_op).await?);
374
375 let result = if result.is_err() && issues.iter().any(|i| i.severity <= IssueSeverity::Error) {
376 None
377 } else {
378 Some(result?)
379 };
380
381 Ok((result, issues, diagnostics, effects))
382}