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 napi_derive::napi;
16use once_cell::sync::Lazy;
17use owo_colors::OwoColorize;
18use serde::Serialize;
19use terminal_hyperlink::Hyperlink;
20use turbo_tasks::{
21    TurboTasks, TurboTasksApi,
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};
30use turbopack_core::error::PrettyPrintError;
31
32pub type NextTurboTasks =
33    Arc<TurboTasks<TurboTasksBackend<Either<DefaultBackingStorage, NoopBackingStorage>>>>;
34
35/// A value often wrapped in [`napi::bindgen_prelude::External`] that retains the [TurboTasks]
36/// instance used by Next.js, and [various napi helpers that are passed to us from
37/// JavaScript][NapiNextTurbopackCallbacks].
38///
39/// This is not a [`turbo_tasks::value`], and should only be used within the top-level napi layer.
40/// It should not be passed to a [`turbo_tasks::function`]. For serializable information about the
41/// project, use the [`next_api::project::Project`] type instead.
42///
43/// This type is a wrapper around an [`Arc`] and is therefore cheaply clonable. It is [`Send`] and
44/// [`Sync`].
45#[derive(Clone)]
46pub struct NextTurbopackContext {
47    inner: Arc<NextTurboContextInner>,
48}
49
50struct NextTurboContextInner {
51    turbo_tasks: NextTurboTasks,
52    napi_callbacks: NapiNextTurbopackCallbacks,
53}
54
55impl NextTurbopackContext {
56    pub fn new(turbo_tasks: NextTurboTasks, napi_callbacks: NapiNextTurbopackCallbacks) -> Self {
57        NextTurbopackContext {
58            inner: Arc::new(NextTurboContextInner {
59                turbo_tasks,
60                napi_callbacks,
61            }),
62        }
63    }
64
65    pub fn turbo_tasks(&self) -> &NextTurboTasks {
66        &self.inner.turbo_tasks
67    }
68
69    /// Constructs and throws a `TurbopackInternalError` from within JavaScript. This type is
70    /// defined within Next.js, and passed via [`NapiNextTurbopackCallbacks`]. This should be called
71    /// at the top level (a `napi` function) and only for errors that are not expected to occur an
72    /// indicate a bug in Turbopack or Next.js.
73    ///
74    /// This may log anonymized information about the error to our telemetry service (via the
75    /// JS callback). It may log to stderr and write a log file to disk (in Rust), subject to
76    /// throttling.
77    ///
78    /// The caller should exit immediately with the returned [`napi::Error`] after calling this, as
79    /// it sets a pending exception.
80    ///
81    /// The returned future does not depend on the lifetime of `&self` or `&err`, making it easier
82    /// to compose with [`futures_util::TryFutureExt`] and similar utilities.
83    pub fn throw_turbopack_internal_error(
84        &self,
85        err: &anyhow::Error,
86    ) -> impl Future<Output = napi::Error> + use<> {
87        let this = self.clone();
88        let message = PrettyPrintError(err).to_string();
89        let downcast_root_cause_err = err.root_cause().downcast_ref::<TurboTasksExecutionError>();
90        let panic_location =
91            if let Some(TurboTasksExecutionError::Panic(p)) = downcast_root_cause_err {
92                p.location.clone()
93            } else {
94                None
95            };
96
97        log_internal_error_and_inform(err);
98
99        async move {
100            this.inner
101                .napi_callbacks
102                .throw_turbopack_internal_error
103                .call_async::<()>(Ok(TurbopackInternalErrorOpts {
104                    message,
105                    anonymized_location: panic_location,
106                }))
107                .await
108                .expect_err("throwTurbopackInternalError must throw an error")
109        }
110    }
111
112    /// A utility method that calls [`NextTurbopackContext::throw_turbopack_internal_error`] and
113    /// wraps the [`napi::Error`] in a [`napi::Result`].
114    ///
115    /// The returned future does not depend on the lifetime of `&self` or `&err`, making it easier
116    /// to compose with [`futures_util::TryFutureExt::or_else`].
117    ///
118    /// The returned type uses a generic (`T`), but should be a never type (`!`) once that nightly
119    /// feature is stabilized.
120    pub fn throw_turbopack_internal_result<T>(
121        &self,
122        err: &anyhow::Error,
123    ) -> impl Future<Output = napi::Result<T>> + use<T> {
124        let err_fut = self.throw_turbopack_internal_error(err);
125        async move { Err(err_fut.await) }
126    }
127}
128
129/// A version of [`NapiNextTurbopackCallbacks`] that can accepted as an argument to a napi function.
130///
131/// This can be converted into a [`NapiNextTurbopackCallbacks`] with
132/// [`NapiNextTurbopackCallbacks::from_js`].
133#[napi(object)]
134pub struct NapiNextTurbopackCallbacksJsObject {
135    /// Called when we've encountered a bug in Turbopack and not in the user's code. Constructs and
136    /// throws a `TurbopackInternalError` type. Logs to anonymized telemetry.
137    ///
138    /// As a result of the use of `ErrorStrategy::CalleeHandled`, the first argument is an error if
139    /// there's a runtime conversion error. This should never happen, but if it does, the function
140    /// can throw it instead.
141    #[napi(ts_type = "(conversionError: Error | null, opts: TurbopackInternalErrorOpts) => never")]
142    pub throw_turbopack_internal_error: JsFunction,
143}
144
145/// A collection of helper JavaScript functions passed into
146/// [`crate::next_api::project::project_new`] and stored in the [`NextTurbopackContext`].
147///
148/// This type is [`Send`] and [`Sync`]. Callbacks are wrapped in [`ThreadsafeFunction`].
149pub struct NapiNextTurbopackCallbacks {
150    // It's a little nasty to use a `ThreadsafeFunction` for this, but we don't expect exceptions
151    // to be a hot codepath.
152    //
153    // More ideally, we'd convert the error type in the JS thread after the execution of the future
154    // when resolving the JS `Promise` object. However, doing that would add a lot more boilerplate
155    // to all of our async entrypoints, and would be complicated by `FunctionRef` being `!Send` (I
156    // think it could be `Send`, as long as `napi::Env` is checked at call-time, which it should be
157    // anyways).
158    throw_turbopack_internal_error: ThreadsafeFunction<TurbopackInternalErrorOpts>,
159}
160
161/// Arguments for [`NapiNextTurbopackCallbacks::throw_turbopack_internal_error`].
162#[napi(object)]
163pub struct TurbopackInternalErrorOpts {
164    pub message: String,
165    pub anonymized_location: Option<String>,
166}
167
168impl NapiNextTurbopackCallbacks {
169    pub fn from_js(obj: NapiNextTurbopackCallbacksJsObject) -> napi::Result<Self> {
170        Ok(NapiNextTurbopackCallbacks {
171            throw_turbopack_internal_error: obj
172                .throw_turbopack_internal_error
173                .create_threadsafe_function(0, |ctx| {
174                    // Avoid unpacking the struct into positional arguments, we really want to make
175                    // sure we don't incorrectly order arguments and accidentally log a potentially
176                    // PII-containing message in anonymized telemetry.
177                    Ok(vec![ctx.value])
178                })?,
179        })
180    }
181}
182
183pub fn create_turbo_tasks(
184    output_path: PathBuf,
185    persistent_caching: bool,
186    _memory_limit: usize,
187    dependency_tracking: bool,
188    is_ci: bool,
189    is_short_session: bool,
190) -> Result<NextTurboTasks> {
191    Ok(if persistent_caching {
192        let version_info = GitVersionInfo {
193            describe: env!("VERGEN_GIT_DESCRIBE"),
194            dirty: option_env!("CI").is_none_or(|value| value.is_empty())
195                && env!("VERGEN_GIT_DIRTY") == "true",
196        };
197        let (backing_storage, cache_state) = default_backing_storage(
198            &output_path.join("cache/turbopack"),
199            &version_info,
200            is_ci,
201            is_short_session,
202        )?;
203        let tt = TurboTasks::new(TurboTasksBackend::new(
204            BackendOptions {
205                storage_mode: Some(if std::env::var("TURBO_ENGINE_READ_ONLY").is_ok() {
206                    turbo_tasks_backend::StorageMode::ReadOnly
207                } else {
208                    turbo_tasks_backend::StorageMode::ReadWrite
209                }),
210                dependency_tracking,
211                num_workers: Some(tokio::runtime::Handle::current().metrics().num_workers()),
212                ..Default::default()
213            },
214            Either::Left(backing_storage),
215        ));
216        if let StartupCacheState::Invalidated { reason_code } = cache_state {
217            tt.send_compilation_event(Arc::new(StartupCacheInvalidationEvent { reason_code }));
218        }
219        tt
220    } else {
221        TurboTasks::new(TurboTasksBackend::new(
222            BackendOptions {
223                storage_mode: None,
224                dependency_tracking,
225                ..Default::default()
226            },
227            Either::Right(noop_backing_storage()),
228        ))
229    })
230}
231
232#[derive(Serialize)]
233struct StartupCacheInvalidationEvent {
234    reason_code: Option<String>,
235}
236
237impl CompilationEvent for StartupCacheInvalidationEvent {
238    fn type_name(&self) -> &'static str {
239        "StartupCacheInvalidationEvent"
240    }
241
242    fn severity(&self) -> Severity {
243        Severity::Warning
244    }
245
246    fn message(&self) -> String {
247        let reason_msg = match self.reason_code.as_deref() {
248            Some(invalidation_reasons::PANIC) => {
249                " because we previously detected an internal error in Turbopack"
250            }
251            Some(invalidation_reasons::USER_REQUEST) => " as the result of a user request",
252            _ => "", // ignore unknown reasons
253        };
254        format!(
255            "Turbopack's filesystem cache has been deleted{reason_msg}. Builds or page loads may \
256             be slower as a result."
257        )
258    }
259
260    fn to_json(&self) -> String {
261        serde_json::to_string(self).unwrap()
262    }
263}
264
265static LOG_THROTTLE: Mutex<Option<Instant>> = Mutex::new(None);
266static LOG_DIVIDER: &str = "---------------------------";
267static PANIC_LOG: Lazy<PathBuf> = Lazy::new(|| {
268    let mut path = env::temp_dir();
269    path.push(format!("next-panic-{:x}.log", rand::random::<u128>()));
270    path
271});
272
273/// Log the error to stderr and write a log file to disk, subject to throttling.
274//
275// TODO: Now that we're passing the error to a JS callback, handle this logic in Next.js using the
276// logger there instead of writing directly to stderr.
277pub fn log_internal_error_and_inform(internal_error: &anyhow::Error) {
278    if cfg!(debug_assertions)
279        || env::var("SWC_DEBUG") == Ok("1".to_string())
280        || env::var("CI").is_ok_and(|v| !v.is_empty())
281        // Next's run-tests unsets CI and sets NEXT_TEST_CI
282        || env::var("NEXT_TEST_CI").is_ok_and(|v| !v.is_empty())
283    {
284        eprintln!(
285            "{}: An unexpected Turbopack error occurred:\n{}",
286            "FATAL".red().bold(),
287            PrettyPrintError(internal_error)
288        );
289        return;
290    }
291
292    // hold open this mutex guard to prevent concurrent writes to the file!
293    let mut last_error_time = LOG_THROTTLE.lock().unwrap();
294    if let Some(last_error_time) = last_error_time.as_ref()
295        && last_error_time.elapsed().as_secs() < 1
296    {
297        // Throttle panic logging to once per second
298        return;
299    }
300    *last_error_time = Some(Instant::now());
301
302    let size = std::fs::metadata(PANIC_LOG.as_path()).map(|m| m.len());
303    if let Ok(size) = size
304        && size > 512 * 1024
305    {
306        // Truncate the earliest error from log file if it's larger than 512KB
307        let new_lines = {
308            let log_read = OpenOptions::new()
309                .read(true)
310                .open(PANIC_LOG.as_path())
311                .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
312
313            io::BufReader::new(&log_read)
314                .lines()
315                .skip(1)
316                .skip_while(|line| match line {
317                    Ok(line) => !line.starts_with(LOG_DIVIDER),
318                    Err(_) => false,
319                })
320                .collect::<Vec<_>>()
321        };
322
323        let mut log_write = OpenOptions::new()
324            .create(true)
325            .truncate(true)
326            .write(true)
327            .open(PANIC_LOG.as_path())
328            .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
329
330        for line in new_lines {
331            match line {
332                Ok(line) => {
333                    writeln!(log_write, "{line}").unwrap();
334                }
335                Err(_) => {
336                    break;
337                }
338            }
339        }
340    }
341
342    let mut log_file = OpenOptions::new()
343        .create(true)
344        .append(true)
345        .open(PANIC_LOG.as_path())
346        .unwrap_or_else(|_| panic!("Failed to open {}", PANIC_LOG.to_string_lossy()));
347
348    let internal_error_str: String = PrettyPrintError(internal_error).to_string();
349    writeln!(log_file, "{}\n{}", LOG_DIVIDER, &internal_error_str).unwrap();
350
351    let title = format!(
352        "Turbopack Error: {}",
353        internal_error_str.lines().next().unwrap_or("Unknown")
354    );
355    let version_str = format!(
356        "Turbopack version: `{}`\nNext.js version: `{}`",
357        env!("VERGEN_GIT_DESCRIBE"),
358        env!("NEXTJS_VERSION")
359    );
360    let new_discussion_url = if supports_hyperlinks::supports_hyperlinks() {
361        "clicking here.".hyperlink(
362            format!(
363                "https://github.com/vercel/next.js/discussions/new?category=turbopack-error-report&title={}&body={}&labels=Turbopack,Turbopack%20Panic%20Backtrace",
364                &urlencoding::encode(&title),
365                &urlencoding::encode(&format!("{}\n\nError message:\n```\n{}\n```", &version_str, &internal_error_str))
366            )
367        )
368    } else {
369        format!(
370            "clicking here: https://github.com/vercel/next.js/discussions/new?category=turbopack-error-report&title={}&body={}&labels=Turbopack,Turbopack%20Panic%20Backtrace",
371            &urlencoding::encode(&title),
372            &urlencoding::encode(&format!("{}\n\nError message:\n```\n{}\n```", &version_str, &title))
373        )
374    };
375
376    eprintln!(
377        "\n-----\n{}: An unexpected Turbopack error occurred. A panic log has been written to \
378         {}.\n\nTo help make Turbopack better, report this error by {}\n-----\n",
379        "FATAL".red().bold(),
380        PANIC_LOG.to_string_lossy(),
381        &new_discussion_url
382    );
383}