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 ..Default::default()
211 },
212 Either::Left(backing_storage),
213 ));
214 if let StartupCacheState::Invalidated { reason_code } = cache_state {
215 tt.send_compilation_event(Arc::new(StartupCacheInvalidationEvent { reason_code }));
216 }
217 tt
218 } else {
219 TurboTasks::new(TurboTasksBackend::new(
220 BackendOptions {
221 storage_mode: None,
222 dependency_tracking,
223 ..Default::default()
224 },
225 Either::Right(noop_backing_storage()),
226 ))
227 })
228}
229
230#[derive(Serialize)]
231struct StartupCacheInvalidationEvent {
232 reason_code: Option<String>,
233}
234
235impl CompilationEvent for StartupCacheInvalidationEvent {
236 fn type_name(&self) -> &'static str {
237 "StartupCacheInvalidationEvent"
238 }
239
240 fn severity(&self) -> Severity {
241 Severity::Warning
242 }
243
244 fn message(&self) -> String {
245 let reason_msg = match self.reason_code.as_deref() {
246 Some(invalidation_reasons::PANIC) => {
247 " because we previously detected an internal error in Turbopack"
248 }
249 Some(invalidation_reasons::USER_REQUEST) => " as the result of a user request",
250 _ => "", };
252 format!(
253 "Turbopack's persistent cache has been deleted{reason_msg}. Builds or page loads may \
254 be slower as a result."
255 )
256 }
257
258 fn to_json(&self) -> String {
259 serde_json::to_string(self).unwrap()
260 }
261}
262
263static LOG_THROTTLE: Mutex<Option<Instant>> = Mutex::new(None);
264static LOG_DIVIDER: &str = "---------------------------";
265static PANIC_LOG: Lazy<PathBuf> = Lazy::new(|| {
266 let mut path = env::temp_dir();
267 path.push(format!("next-panic-{:x}.log", rand::random::<u128>()));
268 path
269});
270
271pub fn log_internal_error_and_inform(internal_error: &anyhow::Error) {
276 if cfg!(debug_assertions)
277 || env::var("SWC_DEBUG") == Ok("1".to_string())
278 || env::var("CI").is_ok_and(|v| !v.is_empty())
279 || env::var("NEXT_TEST_CI").is_ok_and(|v| !v.is_empty())
281 {
282 eprintln!(
283 "{}: An unexpected Turbopack error occurred:\n{}",
284 "FATAL".red().bold(),
285 PrettyPrintError(internal_error)
286 );
287 return;
288 }
289
290 let mut last_error_time = LOG_THROTTLE.lock().unwrap();
292 if let Some(last_error_time) = last_error_time.as_ref()
293 && last_error_time.elapsed().as_secs() < 1
294 {
295 return;
297 }
298 *last_error_time = Some(Instant::now());
299
300 let size = std::fs::metadata(PANIC_LOG.as_path()).map(|m| m.len());
301 if let Ok(size) = size
302 && size > 512 * 1024
303 {
304 let new_lines = {
306 let log_read = OpenOptions::new()
307 .read(true)
308 .open(PANIC_LOG.as_path())
309 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
310
311 io::BufReader::new(&log_read)
312 .lines()
313 .skip(1)
314 .skip_while(|line| match line {
315 Ok(line) => !line.starts_with(LOG_DIVIDER),
316 Err(_) => false,
317 })
318 .collect::<Vec<_>>()
319 };
320
321 let mut log_write = OpenOptions::new()
322 .create(true)
323 .truncate(true)
324 .write(true)
325 .open(PANIC_LOG.as_path())
326 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
327
328 for line in new_lines {
329 match line {
330 Ok(line) => {
331 writeln!(log_write, "{line}").unwrap();
332 }
333 Err(_) => {
334 break;
335 }
336 }
337 }
338 }
339
340 let mut log_file = OpenOptions::new()
341 .create(true)
342 .append(true)
343 .open(PANIC_LOG.as_path())
344 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
345
346 let internal_error_str: String = PrettyPrintError(internal_error).to_string();
347 writeln!(log_file, "{}\n{}", LOG_DIVIDER, &internal_error_str).unwrap();
348
349 let title = format!(
350 "Turbopack Error: {}",
351 internal_error_str.lines().next().unwrap_or("Unknown")
352 );
353 let version_str = format!(
354 "Turbopack version: `{}`\nNext.js version: `{}`",
355 env!("VERGEN_GIT_DESCRIBE"),
356 env!("NEXTJS_VERSION")
357 );
358 let new_discussion_url = if supports_hyperlinks::supports_hyperlinks() {
359 "clicking here.".hyperlink(
360 format!(
361 "https://github.com/vercel/next.js/discussions/new?category=turbopack-error-report&title={}&body={}&labels=Turbopack,Turbopack%20Panic%20Backtrace",
362 &urlencoding::encode(&title),
363 &urlencoding::encode(&format!("{}\n\nError message:\n```\n{}\n```", &version_str, &internal_error_str))
364 )
365 )
366 } else {
367 format!(
368 "clicking here: https://github.com/vercel/next.js/discussions/new?category=turbopack-error-report&title={}&body={}&labels=Turbopack,Turbopack%20Panic%20Backtrace",
369 &urlencoding::encode(&title),
370 &urlencoding::encode(&format!("{}\n\nError message:\n```\n{}\n```", &version_str, &title))
371 )
372 };
373
374 eprintln!(
375 "\n-----\n{}: An unexpected Turbopack error occurred. A panic log has been written to \
376 {}.\n\nTo help make Turbopack better, report this error by {}\n-----\n",
377 "FATAL".red().bold(),
378 PANIC_LOG.to_string_lossy(),
379 &new_discussion_url
380 );
381}