1use std::{
4 env,
5 fs::OpenOptions,
6 io::{self, BufRead, Write},
7 path::PathBuf,
8 sync::{Arc, Mutex},
9 time::Instant,
10};
11
12use anyhow::Result;
13use either::Either;
14use napi::{JsFunction, threadsafe_function::ThreadsafeFunction};
15use napi_derive::napi;
16use once_cell::sync::Lazy;
17use owo_colors::OwoColorize;
18use serde::Serialize;
19use terminal_hyperlink::Hyperlink;
20use turbo_tasks::{
21 TurboTasks, TurboTasksApi,
22 backend::TurboTasksExecutionError,
23 message_queue::{CompilationEvent, Severity},
24};
25use turbo_tasks_backend::{
26 BackendOptions, DefaultBackingStorage, GitVersionInfo, NoopBackingStorage, StartupCacheState,
27 TurboTasksBackend, db_invalidation::invalidation_reasons, default_backing_storage,
28 noop_backing_storage,
29};
30use turbopack_core::error::PrettyPrintError;
31
32pub type NextTurboTasks =
33 Arc<TurboTasks<TurboTasksBackend<Either<DefaultBackingStorage, NoopBackingStorage>>>>;
34
35#[derive(Clone)]
46pub struct NextTurbopackContext {
47 inner: Arc<NextTurboContextInner>,
48}
49
50struct NextTurboContextInner {
51 turbo_tasks: NextTurboTasks,
52 napi_callbacks: NapiNextTurbopackCallbacks,
53}
54
55impl NextTurbopackContext {
56 pub fn new(turbo_tasks: NextTurboTasks, napi_callbacks: NapiNextTurbopackCallbacks) -> Self {
57 NextTurbopackContext {
58 inner: Arc::new(NextTurboContextInner {
59 turbo_tasks,
60 napi_callbacks,
61 }),
62 }
63 }
64
65 pub fn turbo_tasks(&self) -> &NextTurboTasks {
66 &self.inner.turbo_tasks
67 }
68
69 pub fn throw_turbopack_internal_error(
84 &self,
85 err: &anyhow::Error,
86 ) -> impl Future<Output = napi::Error> + use<> {
87 let this = self.clone();
88 let message = PrettyPrintError(err).to_string();
89 let downcast_root_cause_err = err.root_cause().downcast_ref::<TurboTasksExecutionError>();
90 let panic_location =
91 if let Some(TurboTasksExecutionError::Panic(p)) = downcast_root_cause_err {
92 p.location.clone()
93 } else {
94 None
95 };
96
97 log_internal_error_and_inform(err);
98
99 async move {
100 this.inner
101 .napi_callbacks
102 .throw_turbopack_internal_error
103 .call_async::<()>(Ok(TurbopackInternalErrorOpts {
104 message,
105 anonymized_location: panic_location,
106 }))
107 .await
108 .expect_err("throwTurbopackInternalError must throw an error")
109 }
110 }
111
112 pub fn throw_turbopack_internal_result<T>(
121 &self,
122 err: &anyhow::Error,
123 ) -> impl Future<Output = napi::Result<T>> + use<T> {
124 let err_fut = self.throw_turbopack_internal_error(err);
125 async move { Err(err_fut.await) }
126 }
127}
128
129#[napi(object)]
134pub struct NapiNextTurbopackCallbacksJsObject {
135 #[napi(ts_type = "(conversionError: Error | null, opts: TurbopackInternalErrorOpts) => never")]
142 pub throw_turbopack_internal_error: JsFunction,
143}
144
145pub struct NapiNextTurbopackCallbacks {
150 throw_turbopack_internal_error: ThreadsafeFunction<TurbopackInternalErrorOpts>,
159}
160
161#[napi(object)]
163pub struct TurbopackInternalErrorOpts {
164 pub message: String,
165 pub anonymized_location: Option<String>,
166}
167
168impl NapiNextTurbopackCallbacks {
169 pub fn from_js(obj: NapiNextTurbopackCallbacksJsObject) -> napi::Result<Self> {
170 Ok(NapiNextTurbopackCallbacks {
171 throw_turbopack_internal_error: obj
172 .throw_turbopack_internal_error
173 .create_threadsafe_function(0, |ctx| {
174 Ok(vec![ctx.value])
178 })?,
179 })
180 }
181}
182
183pub fn create_turbo_tasks(
184 output_path: PathBuf,
185 persistent_caching: bool,
186 _memory_limit: usize,
187 dependency_tracking: bool,
188 is_ci: bool,
189 is_short_session: bool,
190) -> Result<NextTurboTasks> {
191 Ok(if persistent_caching {
192 let version_info = GitVersionInfo {
193 describe: env!("VERGEN_GIT_DESCRIBE"),
194 dirty: option_env!("CI").is_none_or(|value| value.is_empty())
195 && env!("VERGEN_GIT_DIRTY") == "true",
196 };
197 let (backing_storage, cache_state) = default_backing_storage(
198 &output_path.join("cache/turbopack"),
199 &version_info,
200 is_ci,
201 is_short_session,
202 )?;
203 let tt = TurboTasks::new(TurboTasksBackend::new(
204 BackendOptions {
205 storage_mode: Some(if std::env::var("TURBO_ENGINE_READ_ONLY").is_ok() {
206 turbo_tasks_backend::StorageMode::ReadOnly
207 } else {
208 turbo_tasks_backend::StorageMode::ReadWrite
209 }),
210 dependency_tracking,
211 num_workers: Some(tokio::runtime::Handle::current().metrics().num_workers()),
212 ..Default::default()
213 },
214 Either::Left(backing_storage),
215 ));
216 if let StartupCacheState::Invalidated { reason_code } = cache_state {
217 tt.send_compilation_event(Arc::new(StartupCacheInvalidationEvent { reason_code }));
218 }
219 tt
220 } else {
221 TurboTasks::new(TurboTasksBackend::new(
222 BackendOptions {
223 storage_mode: None,
224 dependency_tracking,
225 ..Default::default()
226 },
227 Either::Right(noop_backing_storage()),
228 ))
229 })
230}
231
232#[derive(Serialize)]
233struct StartupCacheInvalidationEvent {
234 reason_code: Option<String>,
235}
236
237impl CompilationEvent for StartupCacheInvalidationEvent {
238 fn type_name(&self) -> &'static str {
239 "StartupCacheInvalidationEvent"
240 }
241
242 fn severity(&self) -> Severity {
243 Severity::Warning
244 }
245
246 fn message(&self) -> String {
247 let reason_msg = match self.reason_code.as_deref() {
248 Some(invalidation_reasons::PANIC) => {
249 " because we previously detected an internal error in Turbopack"
250 }
251 Some(invalidation_reasons::USER_REQUEST) => " as the result of a user request",
252 _ => "", };
254 format!(
255 "Turbopack's filesystem cache has been deleted{reason_msg}. Builds or page loads may \
256 be slower as a result."
257 )
258 }
259
260 fn to_json(&self) -> String {
261 serde_json::to_string(self).unwrap()
262 }
263}
264
265static LOG_THROTTLE: Mutex<Option<Instant>> = Mutex::new(None);
266static LOG_DIVIDER: &str = "---------------------------";
267static PANIC_LOG: Lazy<PathBuf> = Lazy::new(|| {
268 let mut path = env::temp_dir();
269 path.push(format!("next-panic-{:x}.log", rand::random::<u128>()));
270 path
271});
272
273pub fn log_internal_error_and_inform(internal_error: &anyhow::Error) {
278 if cfg!(debug_assertions)
279 || env::var("SWC_DEBUG") == Ok("1".to_string())
280 || env::var("CI").is_ok_and(|v| !v.is_empty())
281 || env::var("NEXT_TEST_CI").is_ok_and(|v| !v.is_empty())
283 {
284 eprintln!(
285 "{}: An unexpected Turbopack error occurred:\n{}",
286 "FATAL".red().bold(),
287 PrettyPrintError(internal_error)
288 );
289 return;
290 }
291
292 let mut last_error_time = LOG_THROTTLE.lock().unwrap();
294 if let Some(last_error_time) = last_error_time.as_ref()
295 && last_error_time.elapsed().as_secs() < 1
296 {
297 return;
299 }
300 *last_error_time = Some(Instant::now());
301
302 let size = std::fs::metadata(PANIC_LOG.as_path()).map(|m| m.len());
303 if let Ok(size) = size
304 && size > 512 * 1024
305 {
306 let new_lines = {
308 let log_read = OpenOptions::new()
309 .read(true)
310 .open(PANIC_LOG.as_path())
311 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
312
313 io::BufReader::new(&log_read)
314 .lines()
315 .skip(1)
316 .skip_while(|line| match line {
317 Ok(line) => !line.starts_with(LOG_DIVIDER),
318 Err(_) => false,
319 })
320 .collect::<Vec<_>>()
321 };
322
323 let mut log_write = OpenOptions::new()
324 .create(true)
325 .truncate(true)
326 .write(true)
327 .open(PANIC_LOG.as_path())
328 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
329
330 for line in new_lines {
331 match line {
332 Ok(line) => {
333 writeln!(log_write, "{line}").unwrap();
334 }
335 Err(_) => {
336 break;
337 }
338 }
339 }
340 }
341
342 let mut log_file = OpenOptions::new()
343 .create(true)
344 .append(true)
345 .open(PANIC_LOG.as_path())
346 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
347
348 let internal_error_str: String = PrettyPrintError(internal_error).to_string();
349 writeln!(log_file, "{}\n{}", LOG_DIVIDER, &internal_error_str).unwrap();
350
351 let title = format!(
352 "Turbopack Error: {}",
353 internal_error_str.lines().next().unwrap_or("Unknown")
354 );
355 let version_str = format!(
356 "Turbopack version: `{}`\nNext.js version: `{}`",
357 env!("VERGEN_GIT_DESCRIBE"),
358 env!("NEXTJS_VERSION")
359 );
360 let new_discussion_url = if supports_hyperlinks::supports_hyperlinks() {
361 "clicking here.".hyperlink(
362 format!(
363 "https://github.com/vercel/next.js/discussions/new?category=turbopack-error-report&title={}&body={}&labels=Turbopack,Turbopack%20Panic%20Backtrace",
364 &urlencoding::encode(&title),
365 &urlencoding::encode(&format!("{}\n\nError message:\n```\n{}\n```", &version_str, &internal_error_str))
366 )
367 )
368 } else {
369 format!(
370 "clicking here: https://github.com/vercel/next.js/discussions/new?category=turbopack-error-report&title={}&body={}&labels=Turbopack,Turbopack%20Panic%20Backtrace",
371 &urlencoding::encode(&title),
372 &urlencoding::encode(&format!("{}\n\nError message:\n```\n{}\n```", &version_str, &title))
373 )
374 };
375
376 eprintln!(
377 "\n-----\n{}: An unexpected Turbopack error occurred. A panic log has been written to \
378 {}.\n\nTo help make Turbopack better, report this error by {}\n-----\n",
379 "FATAL".red().bold(),
380 PANIC_LOG.to_string_lossy(),
381 &new_discussion_url
382 );
383}