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 next_code_frame::{CodeFrameLocation, CodeFrameOptions, Location, render_code_frame};
12use once_cell::sync::Lazy;
13use regex::Regex;
14use rustc_hash::FxHashMap;
15use serde::Serialize;
16use turbo_tasks::{
17 Effects, OperationVc, ReadRef, TaskId, TryJoinIterExt, Vc, VcValueType, get_effects,
18};
19use turbo_tasks_fs::FileContent;
20use turbopack_core::{
21 diagnostics::{Diagnostic, DiagnosticContextExt, PlainDiagnostic},
22 issue::{
23 CollectibleIssuesExt, IssueFilter, IssueSeverity, PlainIssue, PlainIssueSource,
24 PlainSource, StyledString,
25 },
26 source_pos::SourcePos,
27};
28
29use crate::next_api::turbopack_ctx::NextTurbopackContext;
30
31#[derive(Clone)]
43pub struct DetachedVc<T> {
44 turbopack_ctx: NextTurbopackContext,
45 vc: OperationVc<T>,
47}
48
49impl<T> DetachedVc<T> {
50 pub fn new(turbopack_ctx: NextTurbopackContext, vc: OperationVc<T>) -> Self {
51 Self { turbopack_ctx, vc }
52 }
53
54 pub fn turbopack_ctx(&self) -> &NextTurbopackContext {
55 &self.turbopack_ctx
56 }
57}
58
59impl<T> Deref for DetachedVc<T> {
60 type Target = OperationVc<T>;
61
62 fn deref(&self) -> &Self::Target {
63 &self.vc
64 }
65}
66
67pub struct RootTask {
78 turbopack_ctx: NextTurbopackContext,
79 task_id: Option<TaskId>,
80}
81
82impl Drop for RootTask {
83 fn drop(&mut self) {
84 }
86}
87
88#[napi]
89pub fn root_task_dispose(
90 #[napi(ts_arg_type = "{ __napiType: \"RootTask\" }")] mut root_task: External<RootTask>,
91) -> napi::Result<()> {
92 if let Some(task) = root_task.task_id.take() {
93 root_task
94 .turbopack_ctx
95 .turbo_tasks()
96 .dispose_root_task(task);
97 }
98 Ok(())
99}
100
101pub async fn get_issues<T: Send>(
102 source: OperationVc<T>,
103 filter: Vc<IssueFilter>,
104) -> Result<Arc<Vec<ReadRef<PlainIssue>>>> {
105 Ok(Arc::new(
106 source.peek_issues().get_plain_issues(filter).await?,
107 ))
108}
109
110pub async fn get_diagnostics<T: Send>(
115 source: OperationVc<T>,
116) -> Result<Arc<Vec<ReadRef<PlainDiagnostic>>>> {
117 let captured_diags = source.peek_diagnostics().await?;
118 let mut diags = captured_diags
119 .diagnostics
120 .iter()
121 .map(|d| d.into_plain())
122 .try_join()
123 .await?;
124
125 diags.sort();
126
127 Ok(Arc::new(diags))
128}
129
130fn is_internal(file_path: &str) -> bool {
136 static RE: Lazy<Regex> = Lazy::new(|| {
139 Regex::new(
140 r"(?x)
141 # React vendored in Next.js dist/compiled (reactVendoredRe)
142 [/\\]next[/\\]dist[/\\]compiled[/\\](?:react|react-dom|react-server-dom-webpack|react-server-dom-turbopack|scheduler)[/\\]
143 # React in node_modules (reactNodeModulesRe)
144 | node_modules[/\\](?:react|react-dom|scheduler)[/\\]
145 # Next.js internals (nextInternalsRe)
146 | node_modules[/\\]next[/\\]
147 | [/\\]\.next[/\\]static[/\\]chunks[/\\]webpack\.js$
148 | edge-runtime-webpack\.js$
149 | webpack-runtime\.js$
150 ",
151 )
152 .expect("is_internal regex must compile")
153 });
154
155 RE.is_match(file_path)
156}
157
158fn render_issue_code_frame(issue: &PlainIssue) -> Result<Option<String>> {
166 let Some(source) = issue.source.as_ref() else {
167 return Ok(None);
168 };
169 let Some((start, end)) = source.range else {
170 return Ok(None);
171 };
172
173 if is_internal(&issue.file_path) {
174 return Ok(None);
175 }
176
177 let content = match &*source.asset.content {
178 FileContent::Content(c) => {
179 let Ok(content) = c.content().to_str() else {
180 return Ok(None);
181 };
182 content
183 }
184 FileContent::NotFound => return Ok(None),
185 };
186
187 let location = CodeFrameLocation {
189 start: Location {
190 line: (start.line + 1) as usize,
191 column: Some((start.column + 1) as usize),
192 },
193 end: Some(Location {
194 line: (end.line + 1) as usize,
195 column: Some((end.column + 1) as usize),
196 }),
197 };
198
199 render_code_frame(
200 &content,
201 &location,
202 &CodeFrameOptions {
203 color: true,
204 highlight_code: true,
205 max_width: terminal_size::terminal_size()
206 .map(|(w, _)| w.0 as usize)
207 .unwrap_or(100),
208 ..Default::default()
209 },
210 )
211}
212
213#[napi(object)]
214pub struct NapiIssue {
215 pub severity: String,
216 pub stage: String,
217 pub file_path: String,
218 pub title: serde_json::Value,
219 pub description: Option<serde_json::Value>,
220 pub detail: Option<serde_json::Value>,
221 pub source: Option<NapiIssueSource>,
222 pub documentation_link: String,
223 pub import_traces: serde_json::Value,
224 pub code_frame: Option<String>,
227}
228
229impl From<&PlainIssue> for NapiIssue {
230 fn from(issue: &PlainIssue) -> Self {
231 Self {
232 description: issue
233 .description
234 .as_ref()
235 .map(|styled| serde_json::to_value(StyledStringSerialize::from(styled)).unwrap()),
236 stage: issue.stage.to_string(),
237 file_path: issue.file_path.to_string(),
238 detail: issue
239 .detail
240 .as_ref()
241 .map(|styled| serde_json::to_value(StyledStringSerialize::from(styled)).unwrap()),
242 documentation_link: issue.documentation_link.to_string(),
243 severity: issue.severity.as_str().to_string(),
244 source: issue.source.as_ref().map(|source| source.into()),
245 title: serde_json::to_value(StyledStringSerialize::from(&issue.title)).unwrap(),
246 import_traces: serde_json::to_value(&issue.import_traces).unwrap(),
247 code_frame: render_issue_code_frame(issue).unwrap_or_default(),
248 }
249 }
250}
251
252#[derive(Serialize)]
253#[serde(tag = "type", rename_all = "camelCase")]
254pub enum StyledStringSerialize<'a> {
255 Line {
256 value: Vec<StyledStringSerialize<'a>>,
257 },
258 Stack {
259 value: Vec<StyledStringSerialize<'a>>,
260 },
261 Text {
262 value: &'a str,
263 },
264 Code {
265 value: &'a str,
266 },
267 Strong {
268 value: &'a str,
269 },
270}
271
272impl<'a> From<&'a StyledString> for StyledStringSerialize<'a> {
273 fn from(value: &'a StyledString) -> Self {
274 match value {
275 StyledString::Line(parts) => StyledStringSerialize::Line {
276 value: parts.iter().map(|p| p.into()).collect(),
277 },
278 StyledString::Stack(parts) => StyledStringSerialize::Stack {
279 value: parts.iter().map(|p| p.into()).collect(),
280 },
281 StyledString::Text(string) => StyledStringSerialize::Text { value: string },
282 StyledString::Code(string) => StyledStringSerialize::Code { value: string },
283 StyledString::Strong(string) => StyledStringSerialize::Strong { value: string },
284 }
285 }
286}
287
288#[napi(object)]
289pub struct NapiIssueSource {
290 pub source: NapiSource,
291 pub range: Option<NapiIssueSourceRange>,
292}
293
294impl From<&PlainIssueSource> for NapiIssueSource {
295 fn from(
296 PlainIssueSource {
297 asset: source,
298 range,
299 }: &PlainIssueSource,
300 ) -> Self {
301 Self {
302 source: (&**source).into(),
303 range: range.as_ref().map(|range| range.into()),
304 }
305 }
306}
307
308#[napi(object)]
309pub struct NapiIssueSourceRange {
310 pub start: NapiSourcePos,
311 pub end: NapiSourcePos,
312}
313
314impl From<&(SourcePos, SourcePos)> for NapiIssueSourceRange {
315 fn from((start, end): &(SourcePos, SourcePos)) -> Self {
316 Self {
317 start: (*start).into(),
318 end: (*end).into(),
319 }
320 }
321}
322
323#[napi(object)]
324pub struct NapiSource {
325 pub ident: String,
326}
327
328impl From<&PlainSource> for NapiSource {
329 fn from(source: &PlainSource) -> Self {
330 Self {
331 ident: source.ident.to_string(),
332 }
333 }
334}
335
336#[napi(object)]
337pub struct NapiSourcePos {
338 pub line: u32,
339 pub column: u32,
340}
341
342impl From<SourcePos> for NapiSourcePos {
343 fn from(pos: SourcePos) -> Self {
344 Self {
345 line: pos.line,
346 column: pos.column,
347 }
348 }
349}
350
351#[napi(object)]
352pub struct NapiDiagnostic {
353 pub category: String,
354 pub name: String,
355 #[napi(ts_type = "Record<string, string>")]
356 pub payload: FxHashMap<String, String>,
357}
358
359impl NapiDiagnostic {
360 pub fn from(diagnostic: &PlainDiagnostic) -> Self {
361 Self {
362 category: diagnostic.category.to_string(),
363 name: diagnostic.name.to_string(),
364 payload: diagnostic
365 .payload
366 .iter()
367 .map(|(k, v)| (k.to_string(), v.to_string()))
368 .collect(),
369 }
370 }
371}
372
373pub struct TurbopackResult<T: ToNapiValue> {
374 pub result: T,
375 pub issues: Vec<NapiIssue>,
376 pub diagnostics: Vec<NapiDiagnostic>,
377}
378
379impl<T: ToNapiValue> ToNapiValue for TurbopackResult<T> {
380 unsafe fn to_napi_value(
381 env: napi::sys::napi_env,
382 val: Self,
383 ) -> napi::Result<napi::sys::napi_value> {
384 let mut obj = unsafe { napi::Env::from_raw(env).create_object()? };
385
386 let result = unsafe {
387 let result = T::to_napi_value(env, val.result)?;
388 JsUnknown::from_raw(env, result)?
389 };
390 if matches!(result.get_type()?, napi::ValueType::Object) {
391 let result = unsafe { result.cast::<JsObject>() };
393
394 for key in JsObject::keys(&result)? {
395 let value: JsUnknown = result.get_named_property(&key)?;
396 obj.set_named_property(&key, value)?;
397 }
398 }
399
400 obj.set_named_property("issues", val.issues)?;
401 obj.set_named_property("diagnostics", val.diagnostics)?;
402
403 Ok(unsafe { obj.raw() })
404 }
405}
406
407pub fn subscribe<T: 'static + Send + Sync, F: Future<Output = Result<T>> + Send, V: ToNapiValue>(
408 ctx: NextTurbopackContext,
409 func: JsFunction,
410 handler: impl 'static + Sync + Send + Clone + Fn() -> F,
411 mapper: impl 'static + Sync + Send + FnMut(ThreadSafeCallContext<T>) -> napi::Result<Vec<V>>,
412) -> napi::Result<External<RootTask>> {
413 let func: ThreadsafeFunction<T> = func.create_threadsafe_function(0, mapper)?;
414 let task_id = ctx.turbo_tasks().spawn_root_task({
415 let ctx = ctx.clone();
416 move || {
417 let ctx = ctx.clone();
418 let handler = handler.clone();
419 let func = func.clone();
420 async move {
421 let result = handler()
422 .or_else(|e| ctx.throw_turbopack_internal_result(&e))
423 .await;
424
425 let status = func.call(result, ThreadsafeFunctionCallMode::NonBlocking);
426 if !matches!(status, Status::Ok) {
427 let error = anyhow!("Error calling JS function: {}", status);
428 eprintln!("{error}");
429 return Err::<Vc<()>, _>(error);
430 }
431 Ok(Default::default())
432 }
433 }
434 });
435 Ok(External::new(RootTask {
436 turbopack_ctx: ctx,
437 task_id: Some(task_id),
438 }))
439}
440
441pub async fn strongly_consistent_catch_collectables<R: VcValueType + Send>(
444 source_op: OperationVc<R>,
445 filter: Vc<IssueFilter>,
446) -> Result<(
447 Option<ReadRef<R>>,
448 Arc<Vec<ReadRef<PlainIssue>>>,
449 Arc<Vec<ReadRef<PlainDiagnostic>>>,
450 Arc<Effects>,
451)> {
452 let result = source_op.read_strongly_consistent().await;
453 let issues = get_issues(source_op, filter).await?;
454 let diagnostics = get_diagnostics(source_op).await?;
455 let effects = Arc::new(get_effects(source_op).await?);
456
457 let result = if result.is_err() && issues.iter().any(|i| i.severity <= IssueSeverity::Error) {
458 None
459 } else {
460 Some(result?)
461 };
462
463 Ok((result, issues, diagnostics, effects))
464}
465
466#[napi]
467pub fn expand_next_js_template(
468 content: Buffer,
469 template_path: String,
470 next_package_dir_path: String,
471 #[napi(ts_arg_type = "Record<string, string>")] replacements: FxHashMap<String, String>,
472 #[napi(ts_arg_type = "Record<string, string>")] injections: FxHashMap<String, String>,
473 #[napi(ts_arg_type = "Record<string, string | null>")] imports: FxHashMap<
474 String,
475 Option<String>,
476 >,
477) -> napi::Result<String> {
478 Ok(next_taskless::expand_next_js_template(
479 str::from_utf8(&content).context("template content must be valid utf-8")?,
480 &template_path,
481 &next_package_dir_path,
482 replacements.iter().map(|(k, v)| (&**k, &**v)),
483 injections.iter().map(|(k, v)| (&**k, &**v)),
484 imports.iter().map(|(k, v)| (&**k, v.as_deref())),
485 )?)
486}