Skip to main content

next_napi_bindings/next_api/
turbopack_ctx.rs

1//! Utilities for constructing and using the [`NextTurbopackContext`] type.
2
3use 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/// A value often wrapped in [`napi::bindgen_prelude::External`] that retains the [TurboTasks]
35/// instance used by Next.js, and [various napi helpers that are passed to us from
36/// JavaScript][NapiNextTurbopackCallbacks].
37///
38/// This is not a [`turbo_tasks::value`], and should only be used within the top-level napi layer.
39/// It should not be passed to a [`turbo_tasks::function`]. For serializable information about the
40/// project, use the [`next_api::project::Project`] type instead.
41///
42/// This type is a wrapper around an [`Arc`] and is therefore cheaply clonable. It is [`Send`] and
43/// [`Sync`].
44#[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    /// Constructs and throws a `TurbopackInternalError` from within JavaScript. This type is
69    /// defined within Next.js, and passed via [`NapiNextTurbopackCallbacks`]. This should be called
70    /// at the top level (a `napi` function) and only for errors that are not expected to occur an
71    /// indicate a bug in Turbopack or Next.js.
72    ///
73    /// This may log anonymized information about the error to our telemetry service (via the
74    /// JS callback). It may log to stderr and write a log file to disk (in Rust), subject to
75    /// throttling.
76    ///
77    /// The caller should exit immediately with the returned [`napi::Error`] after calling this, as
78    /// it sets a pending exception.
79    ///
80    /// The returned future does not depend on the lifetime of `&self` or `&err`, making it easier
81    /// to compose with [`futures_util::TryFutureExt`] and similar utilities.
82    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    /// A utility method that calls [`NextTurbopackContext::throw_turbopack_internal_error`] and
112    /// wraps the [`napi::Error`] in a [`napi::Result`].
113    ///
114    /// The returned future does not depend on the lifetime of `&self` or `&err`, making it easier
115    /// to compose with [`futures_util::TryFutureExt::or_else`].
116    ///
117    /// The returned type uses a generic (`T`), but should be a never type (`!`) once that nightly
118    /// feature is stabilized.
119    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    /// Calls the `onBeforeDeferredEntries` callback in Node.js if one was provided.
128    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/// A version of [`NapiNextTurbopackCallbacks`] that can accepted as an argument to a napi function.
138///
139/// This can be converted into a [`NapiNextTurbopackCallbacks`] with
140/// [`NapiNextTurbopackCallbacks::from_js`].
141#[napi(object)]
142pub struct NapiNextTurbopackCallbacksJsObject {
143    /// Called when we've encountered a bug in Turbopack and not in the user's code. Constructs and
144    /// throws a `TurbopackInternalError` type. Logs to anonymized telemetry.
145    ///
146    /// As a result of the use of `ErrorStrategy::CalleeHandled`, the first argument is an error if
147    /// there's a runtime conversion error. This should never happen, but if it does, the function
148    /// can throw it instead.
149    #[napi(ts_type = "(conversionError: Error | null, opts: TurbopackInternalErrorOpts) => never")]
150    pub throw_turbopack_internal_error: JsFunction,
151
152    /// Called before deferred entries are processed in a production build.
153    #[napi(ts_type = "() => Promise<void>")]
154    pub on_before_deferred_entries: Option<JsFunction>,
155}
156
157/// A collection of helper JavaScript functions passed into
158/// [`crate::next_api::project::project_new`] and stored in the [`NextTurbopackContext`].
159///
160/// This type is [`Send`] and [`Sync`]. Callbacks are wrapped in [`ThreadsafeFunction`].
161pub struct NapiNextTurbopackCallbacks {
162    // It's a little nasty to use a `ThreadsafeFunction` for this, but we don't expect exceptions
163    // to be a hot codepath.
164    //
165    // More ideally, we'd convert the error type in the JS thread after the execution of the future
166    // when resolving the JS `Promise` object. However, doing that would add a lot more boilerplate
167    // to all of our async entrypoints, and would be complicated by `FunctionRef` being `!Send` (I
168    // think it could be `Send`, as long as `napi::Env` is checked at call-time, which it should be
169    // anyways).
170    throw_turbopack_internal_error: ThreadsafeFunction<TurbopackInternalErrorOpts>,
171    on_before_deferred_entries: Option<ThreadsafeFunction<()>>,
172}
173
174/// Arguments for `NapiNextTurbopackCallbacks::throw_turbopack_internal_error`.
175#[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                    // Avoid unpacking the struct into positional arguments, we really want to make
187                    // sure we don't incorrectly order arguments and accidentally log a potentially
188                    // PII-containing message in anonymized telemetry.
189                    Ok(vec![ctx.value])
190                })?;
191        // Unref so this ThreadsafeFunction doesn't keep the Node.js event loop alive
192        // after shutdown.
193        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            _ => "", // ignore unknown reasons
283        };
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
303/// Log the error to stderr and write a log file to disk, subject to throttling.
304//
305// TODO: Now that we're passing the error to a JS callback, handle this logic in Next.js using the
306// logger there instead of writing directly to stderr.
307pub 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        // Next's run-tests unsets CI and sets NEXT_TEST_CI
312        || 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    // hold open this mutex guard to prevent concurrent writes to the file!
323    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        // Throttle panic logging to once per second
328        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        // Truncate the earliest error from log file if it's larger than 512KB
337        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}