next_swc_napi/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::{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/// 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
128/// A version of [`NapiNextTurbopackCallbacks`] that can accepted as an argument to a napi function.
129///
130/// This can be converted into a [`NapiNextTurbopackCallbacks`] with
131/// [`NapiNextTurbopackCallbacks::from_js`].
132#[napi(object)]
133pub struct NapiNextTurbopackCallbacksJsObject {
134    /// Called when we've encountered a bug in Turbopack and not in the user's code. Constructs and
135    /// throws a `TurbopackInternalError` type. Logs to anonymized telemetry.
136    ///
137    /// As a result of the use of `ErrorStrategy::CalleeHandled`, the first argument is an error if
138    /// there's a runtime conversion error. This should never happen, but if it does, the function
139    /// can throw it instead.
140    #[napi(ts_type = "(conversionError: Error | null, opts: TurbopackInternalErrorOpts) => never")]
141    pub throw_turbopack_internal_error: JsFunction,
142}
143
144/// A collection of helper JavaScript functions passed into
145/// [`crate::next_api::project::project_new`] and stored in the [`NextTurbopackContext`].
146///
147/// This type is [`Send`] and [`Sync`]. Callbacks are wrapped in [`ThreadsafeFunction`].
148pub struct NapiNextTurbopackCallbacks {
149    // It's a little nasty to use a `ThreadsafeFunction` for this, but we don't expect exceptions
150    // to be a hot codepath.
151    //
152    // More ideally, we'd convert the error type in the JS thread after the execution of the future
153    // when resolving the JS `Promise` object. However, doing that would add a lot more boilerplate
154    // to all of our async entrypoints, and would be complicated by `FunctionRef` being `!Send` (I
155    // think it could be `Send`, as long as `napi::Env` is checked at call-time, which it should be
156    // anyways).
157    throw_turbopack_internal_error: ThreadsafeFunction<TurbopackInternalErrorOpts>,
158}
159
160/// Arguments for [`NapiNextTurbopackCallbacks::throw_turbopack_internal_error`].
161#[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                    // Avoid unpacking the struct into positional arguments, we really want to make
174                    // sure we don't incorrectly order arguments and accidentally log a potentially
175                    // PII-containing message in anonymized telemetry.
176                    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            _ => "", // ignore unknown reasons
251        };
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
271/// Log the error to stderr and write a log file to disk, subject to throttling.
272//
273// TODO: Now that we're passing the error to a JS callback, handle this logic in Next.js using the
274// logger there instead of writing directly to stderr.
275pub 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        // Next's run-tests unsets CI and sets NEXT_TEST_CI
280        || 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    // hold open this mutex guard to prevent concurrent writes to the file!
291    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        // Throttle panic logging to once per second
296        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        // Truncate the earliest error from log file if it's larger than 512KB
305        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}