next_napi_bindings/next_api/
turbopack_ctx.rs1use std::{
4 env,
5 fs::OpenOptions,
6 io::{self, BufRead, Write},
7 path::PathBuf,
8 sync::{Arc, LazyLock, Mutex},
9 time::Instant,
10};
11
12use anyhow::Result;
13use napi::{Env, JsFunction, bindgen_prelude::Promise, threadsafe_function::ThreadsafeFunction};
14use napi_derive::napi;
15use owo_colors::OwoColorize;
16use serde::Serialize;
17use terminal_hyperlink::Hyperlink;
18use turbo_tasks::{
19 PrettyPrintError, TurboTasks, TurboTasksCallApi,
20 backend::TurboTasksExecutionError,
21 message_queue::{CompilationEvent, Severity},
22};
23use turbo_tasks_backend::{
24 BackendOptions, EvictionMode, GitVersionInfo, StartupCacheState, TurboTasksBackend,
25 db_invalidation::invalidation_reasons, noop_backing_storage, turbo_backing_storage,
26};
27
28pub type NextTurboTasks = Arc<TurboTasks<TurboTasksBackend>>;
29
30#[derive(Clone)]
41pub struct NextTurbopackContext {
42 inner: Arc<NextTurboContextInner>,
43}
44
45struct NextTurboContextInner {
46 turbo_tasks: NextTurboTasks,
47 napi_callbacks: NapiNextTurbopackCallbacks,
48}
49
50impl NextTurbopackContext {
51 pub fn new(turbo_tasks: NextTurboTasks, napi_callbacks: NapiNextTurbopackCallbacks) -> Self {
52 NextTurbopackContext {
53 inner: Arc::new(NextTurboContextInner {
54 turbo_tasks,
55 napi_callbacks,
56 }),
57 }
58 }
59
60 pub fn turbo_tasks(&self) -> &NextTurboTasks {
61 &self.inner.turbo_tasks
62 }
63
64 pub fn throw_turbopack_internal_error(
79 &self,
80 err: &anyhow::Error,
81 ) -> impl Future<Output = napi::Error> + use<> {
82 let this = self.clone();
83 let message = PrettyPrintError(err).to_string();
84 let downcast_root_cause_err = err.root_cause().downcast_ref::<TurboTasksExecutionError>();
85 let panic_location =
86 if let Some(TurboTasksExecutionError::Panic(p)) = downcast_root_cause_err {
87 p.location.clone()
88 } else {
89 None
90 };
91
92 log_internal_error_and_inform(err);
93
94 async move {
95 this.inner
96 .napi_callbacks
97 .throw_turbopack_internal_error
98 .call_async::<()>(Ok(TurbopackInternalErrorOpts {
99 message,
100 anonymized_location: panic_location,
101 }))
102 .await
103 .expect_err("throwTurbopackInternalError must throw an error")
104 }
105 }
106
107 pub fn throw_turbopack_internal_result<T>(
116 &self,
117 err: &anyhow::Error,
118 ) -> impl Future<Output = napi::Result<T>> + use<T> {
119 let err_fut = self.throw_turbopack_internal_error(err);
120 async move { Err(err_fut.await) }
121 }
122
123 pub async fn on_before_deferred_entries(&self) -> napi::Result<()> {
125 if let Some(callback) = &self.inner.napi_callbacks.on_before_deferred_entries {
126 let promise = callback.call_async::<Promise<()>>(Ok(())).await?;
127 promise.await?;
128 }
129 Ok(())
130 }
131}
132
133#[napi(object)]
138pub struct NapiNextTurbopackCallbacksJsObject {
139 #[napi(ts_type = "(conversionError: Error | null, opts: TurbopackInternalErrorOpts) => never")]
146 pub throw_turbopack_internal_error: JsFunction,
147
148 #[napi(ts_type = "() => Promise<void>")]
150 pub on_before_deferred_entries: Option<JsFunction>,
151}
152
153pub struct NapiNextTurbopackCallbacks {
158 throw_turbopack_internal_error: ThreadsafeFunction<TurbopackInternalErrorOpts>,
167 on_before_deferred_entries: Option<ThreadsafeFunction<()>>,
168}
169
170#[napi(object)]
172pub struct TurbopackInternalErrorOpts {
173 pub message: String,
174 pub anonymized_location: Option<String>,
175}
176
177impl NapiNextTurbopackCallbacks {
178 pub fn from_js(env: &Env, obj: NapiNextTurbopackCallbacksJsObject) -> napi::Result<Self> {
179 let mut throw_turbopack_internal_error: ThreadsafeFunction<TurbopackInternalErrorOpts> =
180 obj.throw_turbopack_internal_error
181 .create_threadsafe_function(0, |ctx| {
182 Ok(vec![ctx.value])
186 })?;
187 let _ = throw_turbopack_internal_error.unref(env);
190
191 let on_before_deferred_entries = obj
192 .on_before_deferred_entries
193 .map(|callback| {
194 let mut f = callback.create_threadsafe_function(0, |_| Ok::<Vec<()>, _>(vec![]))?;
195 let _ = f.unref(env);
196 Ok::<_, napi::Error>(f)
197 })
198 .transpose()?;
199
200 Ok(NapiNextTurbopackCallbacks {
201 throw_turbopack_internal_error,
202 on_before_deferred_entries,
203 })
204 }
205}
206
207pub fn cache_describe(next_version: &str) -> String {
210 format!("v{next_version}-{}", env!("VERGEN_GIT_SHA"))
211}
212
213pub fn git_version_info(describe: &str) -> GitVersionInfo<'_> {
218 GitVersionInfo {
219 describe,
220 dirty: option_env!("CI").is_none_or(|value| value.is_empty())
221 && env!("VERGEN_GIT_DIRTY") == "true",
222 }
223}
224
225#[napi(string_enum = "lowercase")]
232#[derive(Debug, PartialEq, Eq)]
233pub enum MemoryEvictionMode {
234 Off,
236 Auto,
239 Full,
242}
243
244impl From<MemoryEvictionMode> for EvictionMode {
245 fn from(mode: MemoryEvictionMode) -> Self {
246 match mode {
247 MemoryEvictionMode::Off => EvictionMode::Off,
248 MemoryEvictionMode::Auto => EvictionMode::Auto,
249 MemoryEvictionMode::Full => EvictionMode::Full,
250 }
251 }
252}
253
254pub fn create_turbo_tasks(
255 output_path: PathBuf,
256 next_version: &str,
257 persistent_caching: bool,
258 dependency_tracking: bool,
259 is_ci: bool,
260 is_short_session: bool,
261 skip_compaction: bool,
262 turbopack_memory_eviction: MemoryEvictionMode,
263) -> Result<NextTurboTasks> {
264 Ok(if persistent_caching {
265 let describe = cache_describe(next_version);
266 let version_info = git_version_info(&describe);
267 let (backing_storage, cache_state) = turbo_backing_storage(
268 &output_path.join("cache").join("turbopack"),
269 &version_info,
270 is_ci,
271 is_short_session,
272 skip_compaction,
273 )?;
274 let tt = TurboTasks::new(TurboTasksBackend::new(
275 BackendOptions {
276 storage_mode: Some(if std::env::var("TURBO_ENGINE_READ_ONLY").is_ok() {
277 turbo_tasks_backend::StorageMode::ReadOnly
278 } else if is_ci || is_short_session {
279 turbo_tasks_backend::StorageMode::ReadWriteOnShutdown
280 } else {
281 turbo_tasks_backend::StorageMode::ReadWrite
282 }),
283 dependency_tracking,
284 num_workers: Some(tokio::runtime::Handle::current().metrics().num_workers()),
285 eviction_mode: EvictionMode::from(turbopack_memory_eviction),
286 ..Default::default()
287 },
288 backing_storage,
289 ));
290 if let StartupCacheState::Invalidated { reason_code } = cache_state {
291 tt.send_compilation_event(Arc::new(StartupCacheInvalidationEvent { reason_code }));
292 }
293 tt
294 } else {
295 TurboTasks::new(TurboTasksBackend::new(
296 BackendOptions {
297 storage_mode: None,
298 dependency_tracking,
299 ..Default::default()
300 },
301 noop_backing_storage(),
302 ))
303 })
304}
305
306#[derive(Serialize)]
307struct StartupCacheInvalidationEvent {
308 reason_code: Option<String>,
309}
310
311impl CompilationEvent for StartupCacheInvalidationEvent {
312 fn type_name(&self) -> &'static str {
313 "StartupCacheInvalidationEvent"
314 }
315
316 fn severity(&self) -> Severity {
317 Severity::Warning
318 }
319
320 fn message(&self) -> String {
321 let reason_msg = match self.reason_code.as_deref() {
322 Some(invalidation_reasons::PANIC) => {
323 " because we previously detected an internal error in Turbopack"
324 }
325 Some(invalidation_reasons::USER_REQUEST) => " as the result of a user request",
326 _ => "", };
328 format!(
329 "Turbopack's filesystem cache has been deleted{reason_msg}. Builds or page loads may \
330 be slower as a result."
331 )
332 }
333
334 fn to_json(&self) -> String {
335 serde_json::to_string(self).unwrap()
336 }
337}
338
339static LOG_THROTTLE: Mutex<Option<Instant>> = Mutex::new(None);
340static LOG_DIVIDER: &str = "---------------------------";
341static PANIC_LOG: LazyLock<PathBuf> = LazyLock::new(|| {
342 let mut path = env::temp_dir();
343 path.push(format!("next-panic-{:x}.log", rand::random::<u128>()));
344 path
345});
346
347pub fn log_internal_error_and_inform(internal_error: &anyhow::Error) {
352 if cfg!(debug_assertions)
353 || env::var("SWC_DEBUG") == Ok("1".to_string())
354 || env::var("CI").is_ok_and(|v| !v.is_empty())
355 || env::var("NEXT_TEST_CI").is_ok_and(|v| !v.is_empty())
357 {
358 eprintln!(
359 "{}: An unexpected Turbopack error occurred:\n{}",
360 "FATAL".red().bold(),
361 PrettyPrintError(internal_error)
362 );
363 return;
364 }
365
366 let mut last_error_time = LOG_THROTTLE.lock().unwrap();
368 if let Some(last_error_time) = last_error_time.as_ref()
369 && last_error_time.elapsed().as_secs() < 1
370 {
371 return;
373 }
374 *last_error_time = Some(Instant::now());
375
376 let size = std::fs::metadata(PANIC_LOG.as_path()).map(|m| m.len());
377 if let Ok(size) = size
378 && size > 512 * 1024
379 {
380 let new_lines = {
382 let log_read = OpenOptions::new()
383 .read(true)
384 .open(PANIC_LOG.as_path())
385 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
386
387 io::BufReader::new(&log_read)
388 .lines()
389 .skip(1)
390 .skip_while(|line| match line {
391 Ok(line) => !line.starts_with(LOG_DIVIDER),
392 Err(_) => false,
393 })
394 .collect::<Vec<_>>()
395 };
396
397 let mut log_write = OpenOptions::new()
398 .create(true)
399 .truncate(true)
400 .write(true)
401 .open(PANIC_LOG.as_path())
402 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
403
404 for line in new_lines {
405 match line {
406 Ok(line) => {
407 writeln!(log_write, "{line}").unwrap();
408 }
409 Err(_) => {
410 break;
411 }
412 }
413 }
414 }
415
416 let mut log_file = OpenOptions::new()
417 .create(true)
418 .append(true)
419 .open(PANIC_LOG.as_path())
420 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
421
422 let internal_error_str: String = PrettyPrintError(internal_error).to_string();
423 writeln!(log_file, "{}\n{}", LOG_DIVIDER, internal_error_str).unwrap();
424
425 let title = format!(
426 "Turbopack Error: {}",
427 internal_error_str.lines().next().unwrap_or("Unknown")
428 );
429 let version_str = format!(
430 "Turbopack version: `{}`\nNext.js version: `{}`",
431 env!("VERGEN_GIT_SHA"),
432 env!("NEXTJS_VERSION")
433 );
434 let bug_report_url = format!(
435 "https://bugs.nextjs.org/search?category=turbopack-error-report&title={}&body={}&labels=Turbopack,Turbopack%20Panic%20Backtrace",
436 urlencoding::encode(&title),
437 urlencoding::encode(&format!(
438 "{}\n\nError message:\n```\n{}\n```",
439 version_str, internal_error_str
440 ))
441 );
442 let bug_report_message = if supports_hyperlinks::supports_hyperlinks() {
443 "clicking here.".hyperlink(&bug_report_url)
444 } else {
445 format!("clicking here: {}", bug_report_url)
446 };
447
448 eprintln!(
449 "\n-----\n{}: An unexpected Turbopack error occurred. A panic log has been written to \
450 {}.\n\nTo help make Turbopack better, report this error by {}\n-----\n",
451 "FATAL".red().bold(),
452 PANIC_LOG.to_string_lossy(),
453 bug_report_message
454 );
455}