next_swc_napi/next_api/
utils.rs

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/// An [`OperationVc`] that can be passed back and forth to JS across the [`napi`][mod@napi]
27/// boundary via [`External`].
28///
29/// It is a helper type to hold both a [`OperationVc`] and the [`NextTurbopackContext`]. Without
30/// this, we'd need to pass both individually all over the place.
31///
32/// This napi-specific abstraction does not implement [`turbo_tasks::NonLocalValue`] or
33/// [`turbo_tasks::OperationValue`] and should be dereferenced to an [`OperationVc`] before being
34/// passed to a [`turbo_tasks::function`].
35//
36// TODO: If we add a tracing garbage collector to turbo-tasks, this should be tracked as a GC root.
37#[derive(Clone)]
38pub struct DetachedVc<T> {
39    turbopack_ctx: NextTurbopackContext,
40    /// The Vc. Must be unresolved, otherwise you are referencing an inactive operation.
41    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
69/// An opaque handle to the root of a turbo-tasks computation created by
70/// [`turbo_tasks::TurboTasks::spawn_root_task`] that can be passed back and forth to JS across the
71/// [`napi`][mod@napi] boundary via [`External`].
72///
73/// JavaScript code receiving this value **must** call [`root_task_dispose`] in a `try...finally`
74/// block to avoid leaking root tasks.
75///
76/// This is used by [`subscribe`] to create a computation that re-executes when dependencies change.
77//
78// TODO: If we add a tracing garbage collector to turbo-tasks, this should be tracked as a GC root.
79pub struct RootTask {
80    turbopack_ctx: NextTurbopackContext,
81    task_id: Option<TaskId>,
82}
83
84impl Drop for RootTask {
85    fn drop(&mut self) {
86        // TODO stop the root task
87    }
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
108/// Reads the [turbopack_core::diagnostics::Diagnostic] held
109/// by the given source and returns it as a
110/// [turbopack_core::diagnostics::PlainDiagnostic]. It does
111/// not consume any Diagnostics held by the source.
112pub 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            // SAFETY: We know that result is an object, so we can cast it to a JsObject
311            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
360// Await the source and return fatal issues if there are any, otherwise
361// propagate any actual error results.
362pub 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}