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, Mutex},
9 time::Instant,
10};
11
12use anyhow::Result;
13use either::Either;
14use napi::{Env, JsFunction, bindgen_prelude::Promise, 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 PrettyPrintError, TurboTasks, TurboTasksCallApi,
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};
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 pub async fn on_before_deferred_entries(&self) -> napi::Result<()> {
129 if let Some(callback) = &self.inner.napi_callbacks.on_before_deferred_entries {
130 let promise = callback.call_async::<Promise<()>>(Ok(())).await?;
131 promise.await?;
132 }
133 Ok(())
134 }
135}
136
137#[napi(object)]
142pub struct NapiNextTurbopackCallbacksJsObject {
143 #[napi(ts_type = "(conversionError: Error | null, opts: TurbopackInternalErrorOpts) => never")]
150 pub throw_turbopack_internal_error: JsFunction,
151
152 #[napi(ts_type = "() => Promise<void>")]
154 pub on_before_deferred_entries: Option<JsFunction>,
155}
156
157pub struct NapiNextTurbopackCallbacks {
162 throw_turbopack_internal_error: ThreadsafeFunction<TurbopackInternalErrorOpts>,
171 on_before_deferred_entries: Option<ThreadsafeFunction<()>>,
172}
173
174#[napi(object)]
176pub struct TurbopackInternalErrorOpts {
177 pub message: String,
178 pub anonymized_location: Option<String>,
179}
180
181impl NapiNextTurbopackCallbacks {
182 pub fn from_js(env: &Env, obj: NapiNextTurbopackCallbacksJsObject) -> napi::Result<Self> {
183 let mut throw_turbopack_internal_error: ThreadsafeFunction<TurbopackInternalErrorOpts> =
184 obj.throw_turbopack_internal_error
185 .create_threadsafe_function(0, |ctx| {
186 Ok(vec![ctx.value])
190 })?;
191 let _ = throw_turbopack_internal_error.unref(env);
194
195 let on_before_deferred_entries = obj
196 .on_before_deferred_entries
197 .map(|callback| {
198 let mut f = callback.create_threadsafe_function(0, |_| Ok::<Vec<()>, _>(vec![]))?;
199 let _ = f.unref(env);
200 Ok::<_, napi::Error>(f)
201 })
202 .transpose()?;
203
204 Ok(NapiNextTurbopackCallbacks {
205 throw_turbopack_internal_error,
206 on_before_deferred_entries,
207 })
208 }
209}
210
211pub fn create_turbo_tasks(
212 output_path: PathBuf,
213 persistent_caching: bool,
214 _memory_limit: usize,
215 dependency_tracking: bool,
216 is_ci: bool,
217 is_short_session: bool,
218) -> Result<NextTurboTasks> {
219 Ok(if persistent_caching {
220 let version_info = GitVersionInfo {
221 describe: env!("VERGEN_GIT_DESCRIBE"),
222 dirty: option_env!("CI").is_none_or(|value| value.is_empty())
223 && env!("VERGEN_GIT_DIRTY") == "true",
224 };
225 let (backing_storage, cache_state) = default_backing_storage(
226 &output_path.join("cache/turbopack"),
227 &version_info,
228 is_ci,
229 is_short_session,
230 )?;
231 let tt = TurboTasks::new(TurboTasksBackend::new(
232 BackendOptions {
233 storage_mode: Some(if std::env::var("TURBO_ENGINE_READ_ONLY").is_ok() {
234 turbo_tasks_backend::StorageMode::ReadOnly
235 } else if is_ci || is_short_session {
236 turbo_tasks_backend::StorageMode::ReadWriteOnShutdown
237 } else {
238 turbo_tasks_backend::StorageMode::ReadWrite
239 }),
240 dependency_tracking,
241 num_workers: Some(tokio::runtime::Handle::current().metrics().num_workers()),
242 ..Default::default()
243 },
244 Either::Left(backing_storage),
245 ));
246 if let StartupCacheState::Invalidated { reason_code } = cache_state {
247 tt.send_compilation_event(Arc::new(StartupCacheInvalidationEvent { reason_code }));
248 }
249 tt
250 } else {
251 TurboTasks::new(TurboTasksBackend::new(
252 BackendOptions {
253 storage_mode: None,
254 dependency_tracking,
255 ..Default::default()
256 },
257 Either::Right(noop_backing_storage()),
258 ))
259 })
260}
261
262#[derive(Serialize)]
263struct StartupCacheInvalidationEvent {
264 reason_code: Option<String>,
265}
266
267impl CompilationEvent for StartupCacheInvalidationEvent {
268 fn type_name(&self) -> &'static str {
269 "StartupCacheInvalidationEvent"
270 }
271
272 fn severity(&self) -> Severity {
273 Severity::Warning
274 }
275
276 fn message(&self) -> String {
277 let reason_msg = match self.reason_code.as_deref() {
278 Some(invalidation_reasons::PANIC) => {
279 " because we previously detected an internal error in Turbopack"
280 }
281 Some(invalidation_reasons::USER_REQUEST) => " as the result of a user request",
282 _ => "", };
284 format!(
285 "Turbopack's filesystem cache has been deleted{reason_msg}. Builds or page loads may \
286 be slower as a result."
287 )
288 }
289
290 fn to_json(&self) -> String {
291 serde_json::to_string(self).unwrap()
292 }
293}
294
295static LOG_THROTTLE: Mutex<Option<Instant>> = Mutex::new(None);
296static LOG_DIVIDER: &str = "---------------------------";
297static PANIC_LOG: Lazy<PathBuf> = Lazy::new(|| {
298 let mut path = env::temp_dir();
299 path.push(format!("next-panic-{:x}.log", rand::random::<u128>()));
300 path
301});
302
303pub fn log_internal_error_and_inform(internal_error: &anyhow::Error) {
308 if cfg!(debug_assertions)
309 || env::var("SWC_DEBUG") == Ok("1".to_string())
310 || env::var("CI").is_ok_and(|v| !v.is_empty())
311 || env::var("NEXT_TEST_CI").is_ok_and(|v| !v.is_empty())
313 {
314 eprintln!(
315 "{}: An unexpected Turbopack error occurred:\n{}",
316 "FATAL".red().bold(),
317 PrettyPrintError(internal_error)
318 );
319 return;
320 }
321
322 let mut last_error_time = LOG_THROTTLE.lock().unwrap();
324 if let Some(last_error_time) = last_error_time.as_ref()
325 && last_error_time.elapsed().as_secs() < 1
326 {
327 return;
329 }
330 *last_error_time = Some(Instant::now());
331
332 let size = std::fs::metadata(PANIC_LOG.as_path()).map(|m| m.len());
333 if let Ok(size) = size
334 && size > 512 * 1024
335 {
336 let new_lines = {
338 let log_read = OpenOptions::new()
339 .read(true)
340 .open(PANIC_LOG.as_path())
341 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
342
343 io::BufReader::new(&log_read)
344 .lines()
345 .skip(1)
346 .skip_while(|line| match line {
347 Ok(line) => !line.starts_with(LOG_DIVIDER),
348 Err(_) => false,
349 })
350 .collect::<Vec<_>>()
351 };
352
353 let mut log_write = OpenOptions::new()
354 .create(true)
355 .truncate(true)
356 .write(true)
357 .open(PANIC_LOG.as_path())
358 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
359
360 for line in new_lines {
361 match line {
362 Ok(line) => {
363 writeln!(log_write, "{line}").unwrap();
364 }
365 Err(_) => {
366 break;
367 }
368 }
369 }
370 }
371
372 let mut log_file = OpenOptions::new()
373 .create(true)
374 .append(true)
375 .open(PANIC_LOG.as_path())
376 .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
377
378 let internal_error_str: String = PrettyPrintError(internal_error).to_string();
379 writeln!(log_file, "{}\n{}", LOG_DIVIDER, &internal_error_str).unwrap();
380
381 let title = format!(
382 "Turbopack Error: {}",
383 internal_error_str.lines().next().unwrap_or("Unknown")
384 );
385 let version_str = format!(
386 "Turbopack version: `{}`\nNext.js version: `{}`",
387 env!("VERGEN_GIT_DESCRIBE"),
388 env!("NEXTJS_VERSION")
389 );
390 let bug_report_url = format!(
391 "https://bugs.nextjs.org/search?category=turbopack-error-report&title={}&body={}&labels=Turbopack,Turbopack%20Panic%20Backtrace",
392 &urlencoding::encode(&title),
393 &urlencoding::encode(&format!("{}\n\nError message:\n```\n{}\n```", &version_str, &internal_error_str))
394 );
395 let bug_report_message = if supports_hyperlinks::supports_hyperlinks() {
396 "clicking here.".hyperlink(&bug_report_url)
397 } else {
398 format!("clicking here: {}", bug_report_url)
399 };
400
401 eprintln!(
402 "\n-----\n{}: An unexpected Turbopack error occurred. A panic log has been written to \
403 {}.\n\nTo help make Turbopack better, report this error by {}\n-----\n",
404 "FATAL".red().bold(),
405 PANIC_LOG.to_string_lossy(),
406 &bug_report_message
407 );
408}