next_swc_napi/next_api/
utils.rs

1use std::{future::Future, ops::Deref, sync::Arc};
2
3use anyhow::{Context, Result, anyhow};
4use futures_util::TryFutureExt;
5use napi::{
6    JsFunction, JsObject, JsUnknown, NapiRaw, NapiValue, Status,
7    bindgen_prelude::{Buffer, External, ToNapiValue},
8    threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode},
9};
10use napi_derive::napi;
11use rustc_hash::FxHashMap;
12use serde::Serialize;
13use turbo_tasks::{
14    Effects, OperationVc, ReadRef, TaskId, TryJoinIterExt, Vc, VcValueType, get_effects,
15};
16use turbo_tasks_fs::FileContent;
17use turbopack_core::{
18    diagnostics::{Diagnostic, DiagnosticContextExt, PlainDiagnostic},
19    issue::{
20        CollectibleIssuesExt, IssueSeverity, PlainIssue, PlainIssueSource, PlainSource,
21        StyledString,
22    },
23    source_pos::SourcePos,
24};
25
26use crate::next_api::turbopack_ctx::NextTurbopackContext;
27
28/// An [`OperationVc`] that can be passed back and forth to JS across the [`napi`][mod@napi]
29/// boundary via [`External`].
30///
31/// It is a helper type to hold both a [`OperationVc`] and the [`NextTurbopackContext`]. Without
32/// this, we'd need to pass both individually all over the place.
33///
34/// This napi-specific abstraction does not implement [`turbo_tasks::NonLocalValue`] or
35/// [`turbo_tasks::OperationValue`] and should be dereferenced to an [`OperationVc`] before being
36/// passed to a [`turbo_tasks::function`].
37//
38// TODO: If we add a tracing garbage collector to turbo-tasks, this should be tracked as a GC root.
39#[derive(Clone)]
40pub struct DetachedVc<T> {
41    turbopack_ctx: NextTurbopackContext,
42    /// The Vc. Must be unresolved, otherwise you are referencing an inactive operation.
43    vc: OperationVc<T>,
44}
45
46impl<T> DetachedVc<T> {
47    pub fn new(turbopack_ctx: NextTurbopackContext, vc: OperationVc<T>) -> Self {
48        Self { turbopack_ctx, vc }
49    }
50
51    pub fn turbopack_ctx(&self) -> &NextTurbopackContext {
52        &self.turbopack_ctx
53    }
54}
55
56impl<T> Deref for DetachedVc<T> {
57    type Target = OperationVc<T>;
58
59    fn deref(&self) -> &Self::Target {
60        &self.vc
61    }
62}
63
64pub fn serde_enum_to_string<T: Serialize>(value: &T) -> Result<String> {
65    Ok(serde_json::to_value(value)?
66        .as_str()
67        .context("value must serialize to a string")?
68        .to_string())
69}
70
71/// An opaque handle to the root of a turbo-tasks computation created by
72/// [`turbo_tasks::TurboTasks::spawn_root_task`] that can be passed back and forth to JS across the
73/// [`napi`][mod@napi] boundary via [`External`].
74///
75/// JavaScript code receiving this value **must** call [`root_task_dispose`] in a `try...finally`
76/// block to avoid leaking root tasks.
77///
78/// This is used by [`subscribe`] to create a computation that re-executes when dependencies change.
79//
80// TODO: If we add a tracing garbage collector to turbo-tasks, this should be tracked as a GC root.
81pub struct RootTask {
82    turbopack_ctx: NextTurbopackContext,
83    task_id: Option<TaskId>,
84}
85
86impl Drop for RootTask {
87    fn drop(&mut self) {
88        // TODO stop the root task
89    }
90}
91
92#[napi]
93pub fn root_task_dispose(
94    #[napi(ts_arg_type = "{ __napiType: \"RootTask\" }")] mut root_task: External<RootTask>,
95) -> napi::Result<()> {
96    if let Some(task) = root_task.task_id.take() {
97        root_task
98            .turbopack_ctx
99            .turbo_tasks()
100            .dispose_root_task(task);
101    }
102    Ok(())
103}
104
105pub async fn get_issues<T: Send>(source: OperationVc<T>) -> Result<Arc<Vec<ReadRef<PlainIssue>>>> {
106    Ok(Arc::new(source.peek_issues().get_plain_issues().await?))
107}
108
109/// Reads the [turbopack_core::diagnostics::Diagnostic] held
110/// by the given source and returns it as a
111/// [turbopack_core::diagnostics::PlainDiagnostic]. It does
112/// not consume any Diagnostics held by the source.
113pub async fn get_diagnostics<T: Send>(
114    source: OperationVc<T>,
115) -> Result<Arc<Vec<ReadRef<PlainDiagnostic>>>> {
116    let captured_diags = source.peek_diagnostics().await?;
117    let mut diags = captured_diags
118        .diagnostics
119        .iter()
120        .map(|d| d.into_plain())
121        .try_join()
122        .await?;
123
124    diags.sort();
125
126    Ok(Arc::new(diags))
127}
128
129#[napi(object)]
130pub struct NapiIssue {
131    pub severity: String,
132    pub stage: String,
133    pub file_path: String,
134    pub title: serde_json::Value,
135    pub description: Option<serde_json::Value>,
136    pub detail: Option<serde_json::Value>,
137    pub source: Option<NapiIssueSource>,
138    pub documentation_link: String,
139    pub import_traces: serde_json::Value,
140}
141
142impl From<&PlainIssue> for NapiIssue {
143    fn from(issue: &PlainIssue) -> Self {
144        Self {
145            description: issue
146                .description
147                .as_ref()
148                .map(|styled| serde_json::to_value(StyledStringSerialize::from(styled)).unwrap()),
149            stage: issue.stage.to_string(),
150            file_path: issue.file_path.to_string(),
151            detail: issue
152                .detail
153                .as_ref()
154                .map(|styled| serde_json::to_value(StyledStringSerialize::from(styled)).unwrap()),
155            documentation_link: issue.documentation_link.to_string(),
156            severity: issue.severity.as_str().to_string(),
157            source: issue.source.as_ref().map(|source| source.into()),
158            title: serde_json::to_value(StyledStringSerialize::from(&issue.title)).unwrap(),
159            import_traces: serde_json::to_value(&issue.import_traces).unwrap(),
160        }
161    }
162}
163
164#[derive(Serialize)]
165#[serde(tag = "type", rename_all = "camelCase")]
166pub enum StyledStringSerialize<'a> {
167    Line {
168        value: Vec<StyledStringSerialize<'a>>,
169    },
170    Stack {
171        value: Vec<StyledStringSerialize<'a>>,
172    },
173    Text {
174        value: &'a str,
175    },
176    Code {
177        value: &'a str,
178    },
179    Strong {
180        value: &'a str,
181    },
182}
183
184impl<'a> From<&'a StyledString> for StyledStringSerialize<'a> {
185    fn from(value: &'a StyledString) -> Self {
186        match value {
187            StyledString::Line(parts) => StyledStringSerialize::Line {
188                value: parts.iter().map(|p| p.into()).collect(),
189            },
190            StyledString::Stack(parts) => StyledStringSerialize::Stack {
191                value: parts.iter().map(|p| p.into()).collect(),
192            },
193            StyledString::Text(string) => StyledStringSerialize::Text { value: string },
194            StyledString::Code(string) => StyledStringSerialize::Code { value: string },
195            StyledString::Strong(string) => StyledStringSerialize::Strong { value: string },
196        }
197    }
198}
199
200#[napi(object)]
201pub struct NapiIssueSource {
202    pub source: NapiSource,
203    pub range: Option<NapiIssueSourceRange>,
204}
205
206impl From<&PlainIssueSource> for NapiIssueSource {
207    fn from(
208        PlainIssueSource {
209            asset: source,
210            range,
211        }: &PlainIssueSource,
212    ) -> Self {
213        Self {
214            source: (&**source).into(),
215            range: range.as_ref().map(|range| range.into()),
216        }
217    }
218}
219
220#[napi(object)]
221pub struct NapiIssueSourceRange {
222    pub start: NapiSourcePos,
223    pub end: NapiSourcePos,
224}
225
226impl From<&(SourcePos, SourcePos)> for NapiIssueSourceRange {
227    fn from((start, end): &(SourcePos, SourcePos)) -> Self {
228        Self {
229            start: (*start).into(),
230            end: (*end).into(),
231        }
232    }
233}
234
235#[napi(object)]
236pub struct NapiSource {
237    pub ident: String,
238    pub content: Option<String>,
239}
240
241impl From<&PlainSource> for NapiSource {
242    fn from(source: &PlainSource) -> Self {
243        Self {
244            ident: source.ident.to_string(),
245            content: match &*source.content {
246                FileContent::Content(content) => match content.content().to_str() {
247                    Ok(str) => Some(str.into_owned()),
248                    Err(_) => None,
249                },
250                FileContent::NotFound => None,
251            },
252        }
253    }
254}
255
256#[napi(object)]
257pub struct NapiSourcePos {
258    pub line: u32,
259    pub column: u32,
260}
261
262impl From<SourcePos> for NapiSourcePos {
263    fn from(pos: SourcePos) -> Self {
264        Self {
265            line: pos.line,
266            column: pos.column,
267        }
268    }
269}
270
271#[napi(object)]
272pub struct NapiDiagnostic {
273    pub category: String,
274    pub name: String,
275    #[napi(ts_type = "Record<string, string>")]
276    pub payload: FxHashMap<String, String>,
277}
278
279impl NapiDiagnostic {
280    pub fn from(diagnostic: &PlainDiagnostic) -> Self {
281        Self {
282            category: diagnostic.category.to_string(),
283            name: diagnostic.name.to_string(),
284            payload: diagnostic
285                .payload
286                .iter()
287                .map(|(k, v)| (k.to_string(), v.to_string()))
288                .collect(),
289        }
290    }
291}
292
293pub struct TurbopackResult<T: ToNapiValue> {
294    pub result: T,
295    pub issues: Vec<NapiIssue>,
296    pub diagnostics: Vec<NapiDiagnostic>,
297}
298
299impl<T: ToNapiValue> ToNapiValue for TurbopackResult<T> {
300    unsafe fn to_napi_value(
301        env: napi::sys::napi_env,
302        val: Self,
303    ) -> napi::Result<napi::sys::napi_value> {
304        let mut obj = unsafe { napi::Env::from_raw(env).create_object()? };
305
306        let result = unsafe {
307            let result = T::to_napi_value(env, val.result)?;
308            JsUnknown::from_raw(env, result)?
309        };
310        if matches!(result.get_type()?, napi::ValueType::Object) {
311            // SAFETY: We know that result is an object, so we can cast it to a JsObject
312            let result = unsafe { result.cast::<JsObject>() };
313
314            for key in JsObject::keys(&result)? {
315                let value: JsUnknown = result.get_named_property(&key)?;
316                obj.set_named_property(&key, value)?;
317            }
318        }
319
320        obj.set_named_property("issues", val.issues)?;
321        obj.set_named_property("diagnostics", val.diagnostics)?;
322
323        Ok(unsafe { obj.raw() })
324    }
325}
326
327pub fn subscribe<T: 'static + Send + Sync, F: Future<Output = Result<T>> + Send, V: ToNapiValue>(
328    ctx: NextTurbopackContext,
329    func: JsFunction,
330    handler: impl 'static + Sync + Send + Clone + Fn() -> F,
331    mapper: impl 'static + Sync + Send + FnMut(ThreadSafeCallContext<T>) -> napi::Result<Vec<V>>,
332) -> napi::Result<External<RootTask>> {
333    let func: ThreadsafeFunction<T> = func.create_threadsafe_function(0, mapper)?;
334    let task_id = ctx.turbo_tasks().spawn_root_task({
335        let ctx = ctx.clone();
336        move || {
337            let ctx = ctx.clone();
338            let handler = handler.clone();
339            let func = func.clone();
340            async move {
341                let result = handler()
342                    .or_else(|e| ctx.throw_turbopack_internal_result(&e))
343                    .await;
344
345                let status = func.call(result, ThreadsafeFunctionCallMode::NonBlocking);
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    });
355    Ok(External::new(RootTask {
356        turbopack_ctx: ctx,
357        task_id: Some(task_id),
358    }))
359}
360
361// Await the source and return fatal issues if there are any, otherwise
362// propagate any actual error results.
363pub async fn strongly_consistent_catch_collectables<R: VcValueType + Send>(
364    source_op: OperationVc<R>,
365) -> Result<(
366    Option<ReadRef<R>>,
367    Arc<Vec<ReadRef<PlainIssue>>>,
368    Arc<Vec<ReadRef<PlainDiagnostic>>>,
369    Arc<Effects>,
370)> {
371    let result = source_op.read_strongly_consistent().await;
372    let issues = get_issues(source_op).await?;
373    let diagnostics = get_diagnostics(source_op).await?;
374    let effects = Arc::new(get_effects(source_op).await?);
375
376    let result = if result.is_err() && issues.iter().any(|i| i.severity <= IssueSeverity::Error) {
377        None
378    } else {
379        Some(result?)
380    };
381
382    Ok((result, issues, diagnostics, effects))
383}
384
385#[napi]
386pub fn expand_next_js_template(
387    content: Buffer,
388    template_path: String,
389    next_package_dir_path: String,
390    #[napi(ts_arg_type = "Record<string, string>")] replacements: FxHashMap<String, String>,
391    #[napi(ts_arg_type = "Record<string, string>")] injections: FxHashMap<String, String>,
392    #[napi(ts_arg_type = "Record<string, string | null>")] imports: FxHashMap<
393        String,
394        Option<String>,
395    >,
396) -> napi::Result<String> {
397    Ok(next_taskless::expand_next_js_template(
398        str::from_utf8(&content).context("template content must be valid utf-8")?,
399        &template_path,
400        &next_package_dir_path,
401        replacements.iter().map(|(k, v)| (&**k, &**v)),
402        injections.iter().map(|(k, v)| (&**k, &**v)),
403        imports.iter().map(|(k, v)| (&**k, v.as_deref())),
404    )?)
405}