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