From 787f4276c127e2b7f2dee68bf8b5e52e55803d01 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 16 Apr 2026 03:53:17 +0000 Subject: [PATCH] fix(tracing): avoid SetGlobalDefaultError panic when rolldown devtools is enabled (#1364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes a panic that occurs when `build.rolldownOptions.devtools` is set in `vite.config.ts` (commonly via `@vitejs/devtools`): ``` Rolldown panicked. This is a bug in Rolldown, not your code. thread '' panicked at tracing-subscriber-0.3.23/src/util.rs:94:14: failed to set global default subscriber: SetGlobalDefaultError("a global default trace dispatcher has already been set") ``` ## Root cause The vite-plus NAPI binary bundles `rolldown_binding` (including `rolldown_devtools`) into a single `.node` file. Two paths inside this same binary each try to install a global tracing subscriber: 1. **NAPI module load** → `vite_shared::init_tracing()` — previously installed a no-op subscriber (empty `Targets` filter) even when `VITE_LOG` was unset, occupying the global default slot. 2. **Build with devtools enabled** → `DebugTracer::init()` — tries to install its own subscriber and panics because the slot is already taken. ## Fix - Skip subscriber installation entirely when `VITE_LOG` is not set, leaving the global default slot free for `DebugTracer::init()` (the common case — no one pays for tracing they didn't ask for). - Use `.try_init().ok()` instead of `.init()` so a double-init still fails gracefully rather than panicking. ## Test plan - [x] Reproduced the panic at `/tmp/repro-1356` with `@vitejs/devtools@0.1.13` + `build.rolldownOptions.devtools: {}`. - [x] Rebuilt with `RELEASE_BUILD=1 pnpm bootstrap-cli`; `npx vp build` now completes successfully. - [x] `VITE_LOG=debug vp build` still prints traces (subscriber is installed when requested). ## Related Upstream rolldown also makes the same `.init()` → `.try_init()` change as defense-in-depth: rolldown/rolldown#9078. That upstream fix is not required for this patch — the vite-plus fix here is sufficient on its own. Closes #1356 --- crates/vite_shared/src/tracing.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/vite_shared/src/tracing.rs b/crates/vite_shared/src/tracing.rs index 97c889b11e..e3ebcb15e1 100644 --- a/crates/vite_shared/src/tracing.rs +++ b/crates/vite_shared/src/tracing.rs @@ -1,6 +1,6 @@ //! Tracing initialization for vite-plus -use std::sync::OnceLock; +use std::{str::FromStr, sync::OnceLock}; use tracing_subscriber::{ filter::{LevelFilter, Targets}, @@ -14,25 +14,28 @@ use crate::env_vars; /// Uses `OnceLock` to ensure tracing is only initialized once, /// even if called multiple times. /// +/// Only sets the global default subscriber when `VITE_LOG` is set. +/// When unset, the global default slot is left free so that other +/// subscribers (e.g., rolldown devtools) can claim it without panicking. +/// /// # Environment Variables /// - `VITE_LOG`: Controls log filtering (e.g., "debug", "vite_task=trace") pub fn init_tracing() { static TRACING: OnceLock<()> = OnceLock::new(); TRACING.get_or_init(|| { + let Ok(env_var) = std::env::var(env_vars::VITE_LOG) else { + return; + }; + tracing_subscriber::registry() .with( - std::env::var(env_vars::VITE_LOG) - .map_or_else( - |_| Targets::new(), - |env_var| { - use std::str::FromStr; - Targets::from_str(&env_var).unwrap_or_default() - }, - ) + Targets::from_str(&env_var) + .unwrap_or_default() // disable brush-parser tracing .with_targets([("tokenize", LevelFilter::OFF), ("parse", LevelFilter::OFF)]), ) .with(tracing_subscriber::fmt::layer()) - .init(); + .try_init() + .ok(); }); }