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