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#[derive(Clone)]
40pub struct DetachedVc<T> {
41 turbopack_ctx: NextTurbopackContext,
42 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
71pub struct RootTask {
82 turbopack_ctx: NextTurbopackContext,
83 task_id: Option<TaskId>,
84}
85
86impl Drop for RootTask {
87 fn drop(&mut self) {
88 }
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
109pub 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 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
361pub 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}