From ebe86ea3e9762a780917b0e0a407272fc928a7e5 Mon Sep 17 00:00:00 2001 From: Adrian Darian Date: Wed, 31 Dec 2025 01:32:26 -0800 Subject: [PATCH 1/7] feat(kit): RFC - Unified Logging API proposal This PR proposes a unified logging API for Vite DevTools to replace scattered console.* calls with a consistent, configurable logging system. Includes: - Logger types and interfaces - Node.js implementation (colored terminal output) - Browser/client implementation (styled console) - Log collector for aggregation (enables Logs panel) - Unit tests (14 passing) - RFC documentation with migration examples Related TODOs: - packages/core/src/client/webcomponents/state/setup-script.ts:20 - packages/core/src/node/host-docks.ts:34 --- .../0001-unified-logging-api-examples.txt | 273 +++++++++ docs/rfcs/0001-unified-logging-api.txt | 557 ++++++++++++++++++ packages/kit/package.json | 3 + packages/kit/src/utils/log-collector.ts | 102 ++++ packages/kit/src/utils/logger-client.ts | 131 ++++ packages/kit/src/utils/logger-node.ts | 163 +++++ packages/kit/src/utils/logger-types.ts | 79 +++ packages/kit/src/utils/logger.test.ts | 185 ++++++ packages/kit/src/utils/logger.ts | 65 ++ packages/kit/tsdown.config.ts | 3 + 10 files changed, 1561 insertions(+) create mode 100644 docs/rfcs/0001-unified-logging-api-examples.txt create mode 100644 docs/rfcs/0001-unified-logging-api.txt create mode 100644 packages/kit/src/utils/log-collector.ts create mode 100644 packages/kit/src/utils/logger-client.ts create mode 100644 packages/kit/src/utils/logger-node.ts create mode 100644 packages/kit/src/utils/logger-types.ts create mode 100644 packages/kit/src/utils/logger.test.ts create mode 100644 packages/kit/src/utils/logger.ts diff --git a/docs/rfcs/0001-unified-logging-api-examples.txt b/docs/rfcs/0001-unified-logging-api-examples.txt new file mode 100644 index 00000000..d2b4019b --- /dev/null +++ b/docs/rfcs/0001-unified-logging-api-examples.txt @@ -0,0 +1,273 @@ +# Logger Migration Examples + +This document shows before/after examples of migrating existing `console.*` calls to the unified logger API. + +## Node.js (Server) Examples + +### packages/core/src/node/ws.ts + +**Before:** +```typescript +import c from 'ansis' +import { MARK_INFO } from './constants' + +// ... + +if (isClientAuthDisabled) { + console.warn('[Vite DevTools] Client authentication is disabled. Any browser can connect to the devtools and access to your server and filesystem.') +} + +// ... + +console.log(color`${MARK_INFO} Websocket client connected. [${meta.id}] [${meta.clientAuthId}] (${meta.isTrusted ? 'trusted' : 'untrusted'})`) + +// ... + +console.log(c.red`${MARK_INFO} Websocket client disconnected. [${meta.id}]`) + +// ... + +console.error(c.red`⬢ RPC error on executing "${c.bold(name)}":`) +console.error(error) +``` + +**After:** +```typescript +import { createNodeLogger } from '@vitejs/devtools-kit/utils/logger-node' + +const logger = createNodeLogger({ scope: 'vite-devtools:ws' }) + +// ... + +if (isClientAuthDisabled) { + logger.warn('Client authentication is disabled. Any browser can connect to the devtools and access to your server and filesystem.') +} + +// ... + +logger.info('Websocket client connected', { + id: meta.id, + clientAuthId: meta.clientAuthId, + trusted: meta.isTrusted +}) + +// ... + +logger.info('Websocket client disconnected', { id: meta.id }) + +// ... + +logger.error('RPC error on executing method', { method: name, error }) +``` + +--- + +### packages/core/src/node/context.ts + +**Before:** +```typescript +catch (error) { + console.error(`[Vite DevTools] Error setting up plugin ${plugin.name}:`, error) + throw error +} +``` + +**After:** +```typescript +import { createNodeLogger } from '@vitejs/devtools-kit/utils/logger-node' + +const logger = createNodeLogger({ scope: 'vite-devtools:context' }) + +// ... + +catch (error) { + logger.error(`Error setting up plugin ${plugin.name}`, { + plugin: plugin.name, + error: error as Error + }) + throw error +} +``` + +--- + +### packages/core/src/node/cli-commands.ts + +**Before:** +```typescript +console.log(c.green`${MARK_NODE} Vite DevTools started at`, c.green(`http://${host === '127.0.0.1' ? 'localhost' : host}:${port}`), '\n') + +console.log(c.cyan`${MARK_NODE} Building static Vite DevTools...`) +``` + +**After:** +```typescript +import { createNodeLogger } from '@vitejs/devtools-kit/utils/logger-node' + +const logger = createNodeLogger({ scope: 'vite-devtools:cli' }) + +// ... + +logger.info(`Vite DevTools started at http://${host === '127.0.0.1' ? 'localhost' : host}:${port}`) + +logger.info('Building static Vite DevTools...') +``` + +--- + +## Client (Browser) Examples + +### packages/core/src/client/inject/index.ts + +**Before:** +```typescript +console.log('[VITE DEVTOOLS] Client injected') + +// ... + +console.log('[VITE DEVTOOLS] Skipping in iframe') +``` + +**After:** +```typescript +import { createClientLogger } from '@vitejs/devtools-kit/utils/logger-client' + +const logger = createClientLogger({ scope: 'vite-devtools:inject' }) + +// ... + +logger.info('Client injected') + +// ... + +logger.info('Skipping in iframe') +``` + +--- + +### packages/core/src/client/webcomponents/state/setup-script.ts + +**Before:** +```typescript +.catch((error) => { + // TODO: maybe popup a error toast here? + // TODO: A unified logger API + console.error('[VITE DEVTOOLS] Error executing import action', error) + return Promise.reject(error) +}) +``` + +**After:** +```typescript +import { createClientLogger } from '@vitejs/devtools-kit/utils/logger-client' + +const logger = createClientLogger({ scope: 'vite-devtools:setup-script' }) + +// ... + + .catch((error) => { + logger.error('Error executing import action', { + entryId: id, + error: error as Error + }) + // TODO: integrate with toast notification system + return Promise.reject(error) + }) +``` + +--- + +### packages/vite/src/app/composables/rpc.ts + +**Before:** +```typescript +rpcOptions: { + onGeneralError: (e, name) => { + connectionState.error = e + console.error(`[vite-devtools] RPC error on executing "${name}":`) + }, + onFunctionError: (e, name) => { + connectionState.error = e + console.error(`[vite-devtools] RPC error on executing "${name}":`) + }, +}, +``` + +**After:** +```typescript +import { createClientLogger } from '@vitejs/devtools-kit/utils/logger-client' + +const logger = createClientLogger({ scope: 'vite-devtools:rpc' }) + +// ... + +rpcOptions: { + onGeneralError: (e, name) => { + connectionState.error = e + logger.error(`RPC error on executing "${name}"`, { method: name, error: e }) + }, + onFunctionError: (e, name) => { + connectionState.error = e + logger.error(`RPC error on executing "${name}"`, { method: name, error: e }) + }, +}, +``` + +--- + +## Log Aggregation Integration + +To enable the Logs panel in the DevTools, the context needs to collect logs: + +```typescript +// packages/core/src/node/context.ts +import { createNodeLogger, createLogCollector } from '@vitejs/devtools-kit/utils/logger' + +export async function createDevToolsContext(...) { + // Create log collector for the Logs panel + const logCollector = createLogCollector({ maxEntries: 2000 }) + + // Create logger that feeds into collector + const logger = createNodeLogger({ + scope: 'vite-devtools', + onLog: (entry) => logCollector.add(entry), + }) + + const context: DevToolsNodeContext = { + // ... existing properties + logger, + logs: logCollector, // Expose for RPC/UI + } + + // Add RPC method to fetch logs + context.rpc.register({ + name: 'vite:internal:logs:get', + type: 'action', + setup: () => async (filter) => { + return logCollector.getEntries(filter) + }, + }) + + // Add RPC method to subscribe to live logs (via shared state) + const logsSharedState = await context.rpc.sharedState.get('vite:internal:logs', { + initialValue: [], + }) + + logCollector.subscribe((entries) => { + // Only send last 100 entries for live view + logsSharedState.mutate(() => entries.slice(-100)) + }) +} +``` + +Then enable the Logs panel in `host-docks.ts`: + +```typescript +{ + type: '~builtin', + id: '~logs', + title: 'Logs', + icon: 'ph:notification-duotone', + isHidden: false, // Now enabled! +}, +``` diff --git a/docs/rfcs/0001-unified-logging-api.txt b/docs/rfcs/0001-unified-logging-api.txt new file mode 100644 index 00000000..f4e049df --- /dev/null +++ b/docs/rfcs/0001-unified-logging-api.txt @@ -0,0 +1,557 @@ +# RFC: Unified Logging API for Vite DevTools + +## Summary + +This RFC proposes a unified logging API for the Vite DevTools project to replace scattered `console.*` calls with a consistent, configurable, and feature-rich logging system that works across both Node.js (server) and browser (client) environments. + +## Motivation + +Currently, logging in Vite DevTools is inconsistent: + +1. **Inconsistent prefixes**: Some logs use `[VITE DEVTOOLS]`, others use `[Vite DevTools]`, `[vite-devtools]`, or `⬢` +2. **No log levels**: All logs go to console without filtering capability +3. **No structured logging**: Logs are plain strings with no metadata +4. **No centralized control**: Debug logs use `obug`/`createDebug`, but regular logs use raw `console.*` +5. **No log aggregation**: TODOs in the codebase mention wanting a "Logs" panel (see `host-docks.ts`) +6. **No error toast system**: TODOs mention wanting popup error toasts on the client + +### Current State + +```typescript +// Different prefixes used across the codebase: +console.log('[VITE DEVTOOLS] Client injected') +console.error('[Vite DevTools] Error setting up plugin...') +console.error('[vite-devtools] RPC error on executing...') +console.warn('[Vite DevTools] Client authentication is disabled...') +console.log(c.green`${MARK_NODE} Vite DevTools started at...`) +``` + +## Detailed Design + +### 1. Logger Interface + +```typescript +// packages/kit/src/utils/logger.ts + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent' + +export interface LogEntry { + level: LogLevel + message: string + timestamp: number + scope?: string + meta?: Record + error?: Error +} + +export interface LoggerOptions { + /** Minimum log level to output */ + level?: LogLevel + /** Scope/namespace for the logger (e.g., 'rpc', 'ws', 'client') */ + scope?: string + /** Whether to include timestamps */ + timestamps?: boolean + /** Custom log handler for aggregation */ + onLog?: (entry: LogEntry) => void +} + +export interface Logger { + debug: (message: string, meta?: Record) => void + info: (message: string, meta?: Record) => void + warn: (message: string, meta?: Record) => void + error: (message: string | Error, meta?: Record) => void + + /** Create a child logger with a sub-scope */ + child: (scope: string) => Logger + + /** Update logger options at runtime */ + setLevel: (level: LogLevel) => void +} +``` + +### 2. Node.js Logger Implementation + +```typescript +// packages/kit/src/utils/logger-node.ts + +import c from 'ansis' + +const LOG_LEVEL_PRIORITY: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + silent: 4, +} + +const LEVEL_COLORS = { + debug: c.gray, + info: c.blue, + warn: c.yellow, + error: c.red, +} + +const LEVEL_ICONS = { + debug: '🔍', + info: 'ℹ', + warn: '⚠', + error: '✖', +} + +export function createNodeLogger(options: LoggerOptions = {}): Logger { + const { + level = 'info', + scope = 'vite-devtools', + timestamps = false, + onLog, + } = options + + let currentLevel = level + + function shouldLog(msgLevel: LogLevel): boolean { + return LOG_LEVEL_PRIORITY[msgLevel] >= LOG_LEVEL_PRIORITY[currentLevel] + } + + function formatScope(s: string): string { + return c.cyan`[${s}]` + } + + function log(entry: LogEntry): void { + onLog?.(entry) + + if (!shouldLog(entry.level) || entry.level === 'silent') + return + + const color = LEVEL_COLORS[entry.level] + const icon = LEVEL_ICONS[entry.level] + const parts: string[] = [] + + if (timestamps) { + parts.push(c.gray(new Date(entry.timestamp).toISOString())) + } + + parts.push(color(`${icon}`)) + + if (entry.scope) { + parts.push(formatScope(entry.scope)) + } + + parts.push(entry.message) + + const method = entry.level === 'debug' ? 'log' : entry.level + console[method](parts.join(' ')) + + if (entry.error) { + console.error(entry.error) + } + + if (entry.meta && Object.keys(entry.meta).length > 0) { + console.log(c.gray(' Meta:'), entry.meta) + } + } + + function createLogMethod(level: LogLevel) { + return (message: string | Error, meta?: Record) => { + const entry: LogEntry = { + level, + message: message instanceof Error ? message.message : message, + timestamp: Date.now(), + scope, + meta, + error: message instanceof Error ? message : undefined, + } + log(entry) + } + } + + return { + debug: createLogMethod('debug'), + info: createLogMethod('info'), + warn: createLogMethod('warn'), + error: createLogMethod('error'), + + child(childScope: string): Logger { + return createNodeLogger({ + ...options, + level: currentLevel, + scope: scope ? `${scope}:${childScope}` : childScope, + onLog, + }) + }, + + setLevel(newLevel: LogLevel) { + currentLevel = newLevel + }, + } +} + +// Singleton for convenience +export const logger = createNodeLogger() +``` + +### 3. Client Logger Implementation + +```typescript +// packages/kit/src/utils/logger-client.ts + +import type { LogEntry, Logger, LoggerOptions, LogLevel } from './logger' + +const LOG_LEVEL_PRIORITY: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + silent: 4, +} + +const LEVEL_STYLES = { + debug: 'color: gray', + info: 'color: #3b82f6', + warn: 'color: #f59e0b', + error: 'color: #ef4444; font-weight: bold', +} + +export function createClientLogger(options: LoggerOptions = {}): Logger { + const { + level = 'info', + scope = 'vite-devtools', + timestamps = false, + onLog, + } = options + + let currentLevel = level + + function shouldLog(msgLevel: LogLevel): boolean { + return LOG_LEVEL_PRIORITY[msgLevel] >= LOG_LEVEL_PRIORITY[currentLevel] + } + + function log(entry: LogEntry): void { + onLog?.(entry) + + if (!shouldLog(entry.level) || entry.level === 'silent') + return + + const style = LEVEL_STYLES[entry.level] + const prefix = entry.scope ? `[${entry.scope}]` : '[vite-devtools]' + const time = timestamps ? `${new Date(entry.timestamp).toISOString()} ` : '' + + const method = entry.level === 'debug' ? 'log' : entry.level + console[method]( + `%c${time}${prefix}%c ${entry.message}`, + style, + 'color: inherit', + ...(entry.meta ? [entry.meta] : []), + ) + + if (entry.error) { + console.error(entry.error) + } + } + + function createLogMethod(level: LogLevel) { + return (message: string | Error, meta?: Record) => { + const entry: LogEntry = { + level, + message: message instanceof Error ? message.message : message, + timestamp: Date.now(), + scope, + meta, + error: message instanceof Error ? message : undefined, + } + log(entry) + } + } + + return { + debug: createLogMethod('debug'), + info: createLogMethod('info'), + warn: createLogMethod('warn'), + error: createLogMethod('error'), + + child(childScope: string): Logger { + return createClientLogger({ + ...options, + level: currentLevel, + scope: scope ? `${scope}:${childScope}` : childScope, + onLog, + }) + }, + + setLevel(newLevel: LogLevel) { + currentLevel = newLevel + }, + } +} + +// Singleton for convenience +export const logger = createClientLogger() +``` + +### 4. Universal Entry Point + +```typescript +// packages/kit/src/utils/logger.ts + +export type * from './logger-types' + +// Re-export the appropriate logger based on environment +// This allows tree-shaking and proper bundling + +export function createLogger(options?: LoggerOptions): Logger { + if (typeof window === 'undefined') { + // Node.js environment + const { createNodeLogger } = await import('./logger-node') + return createNodeLogger(options) + } + else { + // Browser environment + const { createClientLogger } = await import('./logger-client') + return createClientLogger(options) + } +} +``` + +### 5. Log Aggregation for DevTools Panel + +To support the "Logs" panel mentioned in the TODO, we can collect logs: + +```typescript +// packages/kit/src/utils/log-collector.ts + +import type { LogEntry } from './logger-types' + +export interface LogCollector { + entries: LogEntry[] + maxEntries: number + + add: (entry: LogEntry) => void + clear: () => void + getEntries: (filter?: { level?: LogLevel, scope?: string }) => LogEntry[] + + // For reactive updates + subscribe: (callback: (entries: LogEntry[]) => void) => () => void +} + +export function createLogCollector(maxEntries = 1000): LogCollector { + const entries: LogEntry[] = [] + const subscribers = new Set<(entries: LogEntry[]) => void>() + + function notify() { + subscribers.forEach(cb => cb([...entries])) + } + + return { + entries, + maxEntries, + + add(entry: LogEntry) { + entries.push(entry) + if (entries.length > maxEntries) { + entries.shift() + } + notify() + }, + + clear() { + entries.length = 0 + notify() + }, + + getEntries(filter) { + return entries.filter((entry) => { + if (filter?.level && entry.level !== filter.level) + return false + if (filter?.scope && !entry.scope?.includes(filter.scope)) + return false + return true + }) + }, + + subscribe(callback) { + subscribers.add(callback) + return () => subscribers.delete(callback) + }, + } +} +``` + +### 6. Integration with DevTools Context + +```typescript +// In packages/core/src/node/context.ts + +import { createNodeLogger, createLogCollector } from '@vitejs/devtools-kit/utils/logger' + +export async function createDevToolsContext(...) { + const logCollector = createLogCollector() + + const logger = createNodeLogger({ + scope: 'vite-devtools', + onLog: (entry) => logCollector.add(entry), + }) + + const context: DevToolsNodeContext = { + // ... existing properties + logger, + logCollector, // Expose for the Logs panel + } + + // Usage in setup + logger.info('DevTools context created', { cwd, mode: viteConfig.command }) + + // For plugins + for (const plugin of plugins) { + const pluginLogger = logger.child(`plugin:${plugin.name}`) + try { + await plugin.devtools?.setup?.(context, pluginLogger) + } catch (error) { + pluginLogger.error(error as Error) + throw error + } + } +} +``` + +### 7. Error Toast System (Client) + +```typescript +// packages/kit/src/client/toast.ts + +import type { LogEntry } from '../utils/logger-types' + +export interface ToastOptions { + duration?: number + type?: 'info' | 'warn' | 'error' | 'success' +} + +export interface ToastManager { + show: (message: string, options?: ToastOptions) => void + showFromLog: (entry: LogEntry) => void + dismiss: (id: string) => void + dismissAll: () => void +} + +// Implementation would integrate with the UI framework (Vue) +// This is just the interface definition +``` + +## Migration Guide + +### Before + +```typescript +// packages/core/src/node/ws.ts +console.warn('[Vite DevTools] Client authentication is disabled...') +console.log(color`${MARK_INFO} Websocket client connected...`) +console.error(c.red`⬢ RPC error on executing...`) +``` + +### After + +```typescript +// packages/core/src/node/ws.ts +import { logger } from '@vitejs/devtools-kit/utils/logger' + +const wsLogger = logger.child('ws') + +wsLogger.warn('Client authentication is disabled. Any browser can connect to the devtools.') +wsLogger.info('Websocket client connected', { id: meta.id, trusted: meta.isTrusted }) +wsLogger.error('RPC error on executing', { method: name, error }) +``` + +## Exports + +The logger should be exported from `@vitejs/devtools-kit`: + +```typescript +// packages/kit/src/index.ts +export { createLogCollector, createLogger } from './utils/logger' +export type { LogCollector, LogEntry, Logger, LoggerOptions, LogLevel } from './utils/logger-types' +``` + +And available via subpath: +```typescript +// For tree-shaking when only logger is needed +import { createLogger } from '@vitejs/devtools-kit/utils/logger' +``` + +## Configuration + +### Environment Variables + +```bash +# Set global log level +VITE_DEVTOOLS_LOG_LEVEL=debug + +# Enable specific scopes (similar to DEBUG env var pattern) +VITE_DEVTOOLS_LOG_SCOPES=vite-devtools:ws,vite-devtools:rpc +``` + +### Runtime Configuration + +```typescript +// In vite.config.ts +export default defineConfig({ + plugins: [ + devtools({ + logger: { + level: 'debug', + timestamps: true, + } + }) + ] +}) +``` + +## Implementation Plan + +### Phase 1: Core Logger (packages/kit) +1. Create logger types in `packages/kit/src/utils/logger-types.ts` +2. Implement Node logger in `packages/kit/src/utils/logger-node.ts` +3. Implement Client logger in `packages/kit/src/utils/logger-client.ts` +4. Create log collector for aggregation +5. Export from kit package + +### Phase 2: Integration (packages/core) +1. Add logger to `DevToolsNodeContext` +2. Migrate `packages/core/src/node/*.ts` console calls +3. Add `onLog` handler for log aggregation + +### Phase 3: Client Integration +1. Migrate `packages/core/src/client/**/*.ts` console calls +2. Implement toast notifications for errors +3. Create Logs panel UI component + +### Phase 4: Cleanup +1. Remove `// TODO: A unified logger API` comments +2. Enable the Logs dock panel (`isHidden: false`) +3. Update documentation + +## Alternatives Considered + +### 1. Use existing library (pino, winston, consola) +- **Pros**: Battle-tested, feature-rich +- **Cons**: Bundle size, over-engineered for devtools needs, may not work well in both Node and browser + +### 2. Extend `obug`/`createDebug` +- **Pros**: Already used in the project +- **Cons**: Debug-only, no log levels, not designed for production logging + +### 3. Keep console.* with standardized prefixes +- **Pros**: Simple, no new dependencies +- **Cons**: No log levels, no aggregation, no structured data + +## Open Questions + +1. Should logs be persisted to disk in dev mode? +2. Should we integrate with Vite's own logger? +3. What should the max buffer size be for log aggregation? +4. Should we support custom log formatters? + +## References + +- [Vite Logger](https://vite.dev/guide/api-javascript.html#custom-logger) +- [consola](https://github.com/unjs/consola) - Similar unified logging approach +- Related TODOs in codebase: + - `packages/core/src/client/webcomponents/state/setup-script.ts:20` + - `packages/core/src/node/host-docks.ts:34` diff --git a/packages/kit/package.json b/packages/kit/package.json index 89121eaa..bb456255 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -24,6 +24,9 @@ "./utils/events": "./dist/utils/events.mjs", "./utils/nanoid": "./dist/utils/nanoid.mjs", "./utils/shared-state": "./dist/utils/shared-state.mjs", + "./utils/logger": "./dist/utils/logger.mjs", + "./utils/logger-node": "./dist/utils/logger-node.mjs", + "./utils/logger-client": "./dist/utils/logger-client.mjs", "./package.json": "./package.json" }, "main": "./dist/index.mjs", diff --git a/packages/kit/src/utils/log-collector.ts b/packages/kit/src/utils/log-collector.ts new file mode 100644 index 00000000..076c74eb --- /dev/null +++ b/packages/kit/src/utils/log-collector.ts @@ -0,0 +1,102 @@ +/** + * Log Collector + * + * Collects and stores log entries for display in the DevTools Logs panel. + * Supports filtering, subscribing to updates, and auto-pruning old entries. + */ + +import type { LogCollector, LogEntry, LogFilter, LogLevel } from './logger-types' + +const LOG_LEVEL_PRIORITY: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + silent: 4, +} + +export interface LogCollectorOptions { + /** Maximum number of entries to keep. Default: 1000 */ + maxEntries?: number +} + +export function createLogCollector(options: LogCollectorOptions = {}): LogCollector { + const { maxEntries = 1000 } = options + + const entries: LogEntry[] = [] + const subscribers = new Set<(entries: readonly LogEntry[]) => void>() + + function notify(): void { + const snapshot = [...entries] as readonly LogEntry[] + subscribers.forEach(cb => cb(snapshot)) + } + + const collector: LogCollector = { + get entries(): readonly LogEntry[] { + return entries + }, + + maxEntries, + + add(entry: LogEntry): void { + entries.push(entry) + + // Prune old entries if over limit + while (entries.length > maxEntries) { + entries.shift() + } + + notify() + }, + + clear(): void { + entries.length = 0 + notify() + }, + + getEntries(filter?: LogFilter): LogEntry[] { + if (!filter) { + return [...entries] + } + + return entries.filter((entry) => { + // Filter by level (entry level must be >= filter level) + if (filter.level) { + const filterPriority = LOG_LEVEL_PRIORITY[filter.level] + const entryPriority = LOG_LEVEL_PRIORITY[entry.level] + if (entryPriority < filterPriority) { + return false + } + } + + // Filter by scope (partial match) + if (filter.scope && entry.scope) { + if (!entry.scope.includes(filter.scope)) { + return false + } + } + + // Filter by timestamp + if (filter.since && entry.timestamp < filter.since) { + return false + } + + return true + }) + }, + + subscribe(callback: (entries: readonly LogEntry[]) => void): () => void { + subscribers.add(callback) + + // Immediately call with current entries + callback([...entries] as readonly LogEntry[]) + + // Return unsubscribe function + return () => { + subscribers.delete(callback) + } + }, + } + + return collector +} diff --git a/packages/kit/src/utils/logger-client.ts b/packages/kit/src/utils/logger-client.ts new file mode 100644 index 00000000..7b5e82bb --- /dev/null +++ b/packages/kit/src/utils/logger-client.ts @@ -0,0 +1,131 @@ +/** + * Logger Implementation for Browser/Client + * + * A lightweight, scoped logger for client-side Vite DevTools code. + * Supports styled console output, log levels, and log aggregation. + */ + +import type { LogEntry, Logger, LoggerOptions, LogLevel } from './logger-types' + +const LOG_LEVEL_PRIORITY: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + silent: 4, +} + +const LEVEL_STYLES: Record, string> = { + debug: 'color: #9ca3af; font-weight: normal', + info: 'color: #3b82f6; font-weight: normal', + warn: 'color: #f59e0b; font-weight: bold', + error: 'color: #ef4444; font-weight: bold', +} + +const LEVEL_ICONS: Record, string> = { + debug: '🔍', + info: 'ℹ️', + warn: '⚠️', + error: '❌', +} + +export function createClientLogger(options: LoggerOptions = {}): Logger { + const { + level = 'info', + scope, + timestamps = false, + onLog, + } = options + + let currentLevel = level + + function shouldLog(msgLevel: LogLevel): boolean { + return LOG_LEVEL_PRIORITY[msgLevel] >= LOG_LEVEL_PRIORITY[currentLevel] + } + + function log(entry: LogEntry): void { + // Always call onLog for aggregation, regardless of level + onLog?.(entry) + + if (!shouldLog(entry.level) || entry.level === 'silent') { + return + } + + const style = LEVEL_STYLES[entry.level] + const icon = LEVEL_ICONS[entry.level] + const scopeText = entry.scope ? `[${entry.scope}]` : '[vite-devtools]' + const time = timestamps ? `${new Date(entry.timestamp).toISOString()} ` : '' + + // Build the formatted message + const prefix = `${time}${icon} ${scopeText}` + + const method = entry.level === 'debug' ? 'log' : entry.level + + // Use styled console output + // eslint-disable-next-line no-console + console[method]( + `%c${prefix}%c ${entry.message}`, + style, + 'color: inherit; font-weight: normal', + ) + + // Log error stack if present + if (entry.error?.stack) { + console.error(entry.error) + } + + // Log metadata if present + if (entry.meta && Object.keys(entry.meta).length > 0) { + // eslint-disable-next-line no-console + console.log(' ↳', entry.meta) + } + } + + function createLogMethod(level: Exclude) { + return (message: string | Error, meta?: Record) => { + const isError = message instanceof Error + const entry: LogEntry = { + level, + message: isError ? message.message : message, + timestamp: Date.now(), + scope, + meta, + error: isError ? message : undefined, + } + log(entry) + } + } + + const logger: Logger = { + debug: createLogMethod('debug'), + info: createLogMethod('info'), + warn: createLogMethod('warn'), + error: createLogMethod('error'), + + child(childScope: string): Logger { + const newScope = scope ? `${scope}:${childScope}` : childScope + return createClientLogger({ + level: currentLevel, + scope: newScope, + timestamps, + onLog, + }) + }, + + setLevel(newLevel: LogLevel) { + currentLevel = newLevel + }, + + getLevel(): LogLevel { + return currentLevel + }, + } + + return logger +} + +/** + * Default logger instance for convenience. + * Use `createClientLogger()` for custom configuration. + */ +export const logger = createClientLogger({ scope: 'vite-devtools' }) diff --git a/packages/kit/src/utils/logger-node.ts b/packages/kit/src/utils/logger-node.ts new file mode 100644 index 00000000..d39fe1c3 --- /dev/null +++ b/packages/kit/src/utils/logger-node.ts @@ -0,0 +1,163 @@ +/** + * Logger Implementation for Node.js + * + * A lightweight, scoped logger for server-side Vite DevTools code. + * Supports colored output, log levels, and log aggregation. + */ + +import type { LogEntry, Logger, LoggerOptions, LogLevel } from './logger-types' +import process from 'node:process' + +const LOG_LEVEL_PRIORITY: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + silent: 4, +} + +// ANSI color codes for terminal output +const COLORS = { + reset: '\x1B[0m', + gray: '\x1B[90m', + cyan: '\x1B[36m', + blue: '\x1B[34m', + yellow: '\x1B[33m', + red: '\x1B[31m', + bold: '\x1B[1m', +} as const + +const LEVEL_CONFIG: Record, { icon: string, color: string }> = { + debug: { icon: '🔍', color: COLORS.gray }, + info: { icon: 'ℹ', color: COLORS.blue }, + warn: { icon: '⚠', color: COLORS.yellow }, + error: { icon: '✖', color: COLORS.red }, +} + +function colorize(text: string, color: string): string { + return `${color}${text}${COLORS.reset}` +} + +function getEnvLogLevel(): LogLevel | undefined { + const envLevel = process.env.VITE_DEVTOOLS_LOG_LEVEL?.toLowerCase() + if (envLevel && envLevel in LOG_LEVEL_PRIORITY) { + return envLevel as LogLevel + } + return undefined +} + +export function createNodeLogger(options: LoggerOptions = {}): Logger { + const { + level = getEnvLogLevel() ?? 'info', + scope, + timestamps = false, + onLog, + } = options + + let currentLevel = level + + function shouldLog(msgLevel: LogLevel): boolean { + return LOG_LEVEL_PRIORITY[msgLevel] >= LOG_LEVEL_PRIORITY[currentLevel] + } + + function formatTimestamp(): string { + return colorize(new Date().toISOString(), COLORS.gray) + } + + function formatScope(s: string): string { + return colorize(`[${s}]`, COLORS.cyan) + } + + function log(entry: LogEntry): void { + // Always call onLog for aggregation, regardless of level + onLog?.(entry) + + if (!shouldLog(entry.level) || entry.level === 'silent') { + return + } + + const config = LEVEL_CONFIG[entry.level] + const parts: string[] = [] + + // Timestamp (optional) + if (timestamps) { + parts.push(formatTimestamp()) + } + + // Icon with color + parts.push(colorize(config.icon, config.color)) + + // Scope + if (entry.scope) { + parts.push(formatScope(entry.scope)) + } + + // Message + parts.push(entry.message) + + // Output + const output = parts.join(' ') + const method = entry.level === 'debug' ? 'log' : entry.level + // eslint-disable-next-line no-console + console[method](output) + + // Error stack trace + if (entry.error?.stack) { + console.error(colorize(entry.error.stack, COLORS.red)) + } + + // Metadata + if (entry.meta && Object.keys(entry.meta).length > 0) { + // eslint-disable-next-line no-console + console.log(colorize(' ↳', COLORS.gray), entry.meta) + } + } + + function createLogMethod(level: Exclude) { + return (message: string | Error, meta?: Record) => { + const isError = message instanceof Error + const entry: LogEntry = { + level, + message: isError ? message.message : message, + timestamp: Date.now(), + scope, + meta, + error: isError ? message : undefined, + } + log(entry) + } + } + + const logger: Logger = { + debug: createLogMethod('debug'), + info: createLogMethod('info'), + warn: createLogMethod('warn'), + error: createLogMethod('error'), + + child(childScope: string): Logger { + const newScope = scope ? `${scope}:${childScope}` : childScope + return createNodeLogger({ + level: currentLevel, + scope: newScope, + timestamps, + onLog, + }) + }, + + setLevel(newLevel: LogLevel) { + currentLevel = newLevel + }, + + getLevel(): LogLevel { + return currentLevel + }, + } + + return logger +} + +/** + * Default logger instance for convenience. + * Use `createNodeLogger()` for custom configuration. + */ +export const logger = createNodeLogger({ scope: 'vite-devtools' }) diff --git a/packages/kit/src/utils/logger-types.ts b/packages/kit/src/utils/logger-types.ts new file mode 100644 index 00000000..210ae4eb --- /dev/null +++ b/packages/kit/src/utils/logger-types.ts @@ -0,0 +1,79 @@ +/** + * Logger Types + * + * Shared type definitions for the unified logging API. + */ + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent' + +export interface LogEntry { + /** Log level */ + level: LogLevel + /** Log message */ + message: string + /** Unix timestamp in milliseconds */ + timestamp: number + /** Logger scope/namespace (e.g., 'rpc', 'ws', 'client') */ + scope?: string + /** Additional structured metadata */ + meta?: Record + /** Error object if this is an error log */ + error?: Error +} + +export interface LoggerOptions { + /** Minimum log level to output. Default: 'info' */ + level?: LogLevel + /** Scope/namespace for the logger (e.g., 'rpc', 'ws', 'client') */ + scope?: string + /** Whether to include timestamps in output. Default: false */ + timestamps?: boolean + /** Custom log handler for aggregation/forwarding */ + onLog?: (entry: LogEntry) => void +} + +export interface Logger { + /** Log debug message (development only) */ + debug: (message: string, meta?: Record) => void + /** Log info message */ + info: (message: string, meta?: Record) => void + /** Log warning message */ + warn: (message: string, meta?: Record) => void + /** Log error message or Error object */ + error: (message: string | Error, meta?: Record) => void + + /** Create a child logger with a sub-scope */ + child: (scope: string) => Logger + + /** Update logger level at runtime */ + setLevel: (level: LogLevel) => void + + /** Get current log level */ + getLevel: () => LogLevel +} + +export interface LogCollector { + /** All collected log entries */ + readonly entries: readonly LogEntry[] + /** Maximum number of entries to keep */ + readonly maxEntries: number + + /** Add a log entry */ + add: (entry: LogEntry) => void + /** Clear all entries */ + clear: () => void + /** Get filtered entries */ + getEntries: (filter?: LogFilter) => LogEntry[] + + /** Subscribe to log updates */ + subscribe: (callback: (entries: readonly LogEntry[]) => void) => () => void +} + +export interface LogFilter { + /** Filter by log level */ + level?: LogLevel + /** Filter by scope (partial match) */ + scope?: string + /** Filter entries after this timestamp */ + since?: number +} diff --git a/packages/kit/src/utils/logger.test.ts b/packages/kit/src/utils/logger.test.ts new file mode 100644 index 00000000..5df4c270 --- /dev/null +++ b/packages/kit/src/utils/logger.test.ts @@ -0,0 +1,185 @@ +/** + * Logger Tests + */ + +import type { LogEntry } from './logger-types' +import { describe, expect, it, vi } from 'vitest' +import { createLogCollector } from './log-collector' +import { createNodeLogger } from './logger-node' + +describe('createNodeLogger', () => { + it('should create a logger with default options', () => { + const logger = createNodeLogger() + expect(logger).toBeDefined() + expect(logger.info).toBeInstanceOf(Function) + expect(logger.warn).toBeInstanceOf(Function) + expect(logger.error).toBeInstanceOf(Function) + expect(logger.debug).toBeInstanceOf(Function) + }) + + it('should respect log level', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + const logger = createNodeLogger({ level: 'warn' }) + + logger.debug('debug message') + logger.info('info message') + + expect(consoleSpy).not.toHaveBeenCalled() + consoleSpy.mockRestore() + }) + + it('should call onLog callback for all levels', () => { + const entries: LogEntry[] = [] + const logger = createNodeLogger({ + level: 'silent', // Don't output to console + onLog: entry => entries.push(entry), + }) + + logger.debug('debug') + logger.info('info') + logger.warn('warn') + logger.error('error') + + expect(entries).toHaveLength(4) + expect(entries.map(e => e.level)).toEqual(['debug', 'info', 'warn', 'error']) + }) + + it('should create child logger with combined scope', () => { + const entries: LogEntry[] = [] + const logger = createNodeLogger({ + scope: 'parent', + level: 'silent', + onLog: entry => entries.push(entry), + }) + + const child = logger.child('child') + child.info('message') + + expect(entries[0].scope).toBe('parent:child') + }) + + it('should handle Error objects', () => { + const entries: LogEntry[] = [] + const logger = createNodeLogger({ + level: 'silent', + onLog: entry => entries.push(entry), + }) + + const error = new Error('test error') + logger.error(error) + + expect(entries[0].message).toBe('test error') + expect(entries[0].error).toBe(error) + }) + + it('should include metadata', () => { + const entries: LogEntry[] = [] + const logger = createNodeLogger({ + level: 'silent', + onLog: entry => entries.push(entry), + }) + + logger.info('message', { key: 'value' }) + + expect(entries[0].meta).toEqual({ key: 'value' }) + }) + + it('should allow changing log level at runtime', () => { + const logger = createNodeLogger({ level: 'info' }) + + expect(logger.getLevel()).toBe('info') + + logger.setLevel('debug') + expect(logger.getLevel()).toBe('debug') + }) +}) + +describe('createLogCollector', () => { + it('should collect log entries', () => { + const collector = createLogCollector() + + collector.add({ + level: 'info', + message: 'test', + timestamp: Date.now(), + }) + + expect(collector.entries).toHaveLength(1) + }) + + it('should respect maxEntries limit', () => { + const collector = createLogCollector({ maxEntries: 3 }) + + for (let i = 0; i < 5; i++) { + collector.add({ + level: 'info', + message: `message ${i}`, + timestamp: Date.now(), + }) + } + + expect(collector.entries).toHaveLength(3) + expect(collector.entries[0].message).toBe('message 2') + expect(collector.entries[2].message).toBe('message 4') + }) + + it('should filter entries by level', () => { + const collector = createLogCollector() + + collector.add({ level: 'debug', message: 'debug', timestamp: Date.now() }) + collector.add({ level: 'info', message: 'info', timestamp: Date.now() }) + collector.add({ level: 'warn', message: 'warn', timestamp: Date.now() }) + collector.add({ level: 'error', message: 'error', timestamp: Date.now() }) + + const warnings = collector.getEntries({ level: 'warn' }) + expect(warnings).toHaveLength(2) // warn and error + }) + + it('should filter entries by scope', () => { + const collector = createLogCollector() + + collector.add({ level: 'info', message: 'a', timestamp: Date.now(), scope: 'rpc' }) + collector.add({ level: 'info', message: 'b', timestamp: Date.now(), scope: 'rpc:call' }) + collector.add({ level: 'info', message: 'c', timestamp: Date.now(), scope: 'ws' }) + + const rpcLogs = collector.getEntries({ scope: 'rpc' }) + expect(rpcLogs).toHaveLength(2) + }) + + it('should notify subscribers on add', () => { + const collector = createLogCollector() + const callback = vi.fn() + + collector.subscribe(callback) + + // Called immediately with current entries + expect(callback).toHaveBeenCalledWith([]) + + collector.add({ level: 'info', message: 'test', timestamp: Date.now() }) + + expect(callback).toHaveBeenCalledTimes(2) + }) + + it('should allow unsubscribing', () => { + const collector = createLogCollector() + const callback = vi.fn() + + const unsubscribe = collector.subscribe(callback) + unsubscribe() + + collector.add({ level: 'info', message: 'test', timestamp: Date.now() }) + + // Only called once (initial call) + expect(callback).toHaveBeenCalledTimes(1) + }) + + it('should clear all entries', () => { + const collector = createLogCollector() + + collector.add({ level: 'info', message: 'test', timestamp: Date.now() }) + expect(collector.entries).toHaveLength(1) + + collector.clear() + expect(collector.entries).toHaveLength(0) + }) +}) diff --git a/packages/kit/src/utils/logger.ts b/packages/kit/src/utils/logger.ts new file mode 100644 index 00000000..f942c037 --- /dev/null +++ b/packages/kit/src/utils/logger.ts @@ -0,0 +1,65 @@ +/** + * Unified Logger API + * + * Entry point for the Vite DevTools logging system. + * Automatically selects the appropriate logger implementation based on environment. + * + * @example + * ```ts + * import { createLogger } from '@vitejs/devtools-kit/utils/logger' + * + * const logger = createLogger({ scope: 'my-plugin' }) + * logger.info('Plugin initialized') + * logger.debug('Debug info', { config }) + * logger.warn('Deprecated option used') + * logger.error(new Error('Something went wrong')) + * + * // Create child loggers for sub-components + * const rpcLogger = logger.child('rpc') + * rpcLogger.info('RPC connected') // [my-plugin:rpc] RPC connected + * ``` + */ + +// Re-export collector +export { createLogCollector } from './log-collector' + +export type { LogCollectorOptions } from './log-collector' +export { logger as clientLogger, createClientLogger } from './logger-client' + +// Environment-specific exports +// These are separate so bundlers can tree-shake the unused implementation + +export { createNodeLogger, logger as nodeLogger } from './logger-node' +// Re-export types +export type { + LogCollector, + LogEntry, + LogFilter, + Logger, + LoggerOptions, + LogLevel, +} from './logger-types' + +/** + * Create a logger instance. + * + * In Node.js: Uses colored terminal output + * In Browser: Uses styled console output + * + * @param options - Logger configuration options + * @returns Logger instance + */ +export function createLogger(options?: import('./logger-types').LoggerOptions): import('./logger-types').Logger { + // Check for browser environment + if (typeof window !== 'undefined') { + // Dynamic import for tree-shaking in Node bundles + // eslint-disable-next-line ts/no-require-imports + const { createClientLogger } = require('./logger-client') + return createClientLogger(options) + } + else { + // eslint-disable-next-line ts/no-require-imports + const { createNodeLogger } = require('./logger-node') + return createNodeLogger(options) + } +} diff --git a/packages/kit/tsdown.config.ts b/packages/kit/tsdown.config.ts index 1d976f3d..d62da0d0 100644 --- a/packages/kit/tsdown.config.ts +++ b/packages/kit/tsdown.config.ts @@ -6,6 +6,9 @@ export default defineConfig({ 'utils/events': 'src/utils/events.ts', 'utils/nanoid': 'src/utils/nanoid.ts', 'utils/shared-state': 'src/utils/shared-state.ts', + 'utils/logger': 'src/utils/logger.ts', + 'utils/logger-node': 'src/utils/logger-node.ts', + 'utils/logger-client': 'src/utils/logger-client.ts', 'client': 'src/client/index.ts', }, exports: true, From d16a01ecc2f283bc69380601f285782fdee29499 Mon Sep 17 00:00:00 2001 From: Adrian Darian Date: Tue, 13 Jan 2026 01:51:37 -0800 Subject: [PATCH 2/7] feat(logger): integrate consola for unified logging API and remove legacy logger implementations --- alias.ts | 1 + .../client/webcomponents/.generated/css.ts | 2 +- .../components/DockEntriesWithCategories.vue | 2 +- packages/core/src/node/context.ts | 2 + packages/kit/package.json | 5 +- packages/kit/src/types/vite-plugin.ts | 18 ++ packages/kit/src/utils/log-collector.ts | 102 ---------- packages/kit/src/utils/logger-client.ts | 131 ------------- packages/kit/src/utils/logger-node.ts | 163 --------------- packages/kit/src/utils/logger-types.ts | 79 -------- packages/kit/src/utils/logger.test.ts | 185 ------------------ packages/kit/src/utils/logger.ts | 77 +++----- packages/kit/tsdown.config.ts | 2 - packages/vite/src/app/composables/dark.ts | 1 + .../vite/src/app/composables/module-graph.ts | 1 + packages/vite/src/app/composables/rpc.ts | 1 + packages/vite/src/app/utils/color.ts | 1 + packages/vite/src/app/utils/filepath.ts | 1 + packages/vite/src/app/utils/icon.ts | 1 + packages/vite/src/shared/types/data.ts | 1 + packages/vite/src/shared/types/vite.ts | 1 + pnpm-lock.yaml | 8 +- pnpm-workspace.yaml | 1 + test/exports/@vitejs/devtools-kit.yaml | 3 + tsconfig.base.json | 3 + 25 files changed, 74 insertions(+), 718 deletions(-) delete mode 100644 packages/kit/src/utils/log-collector.ts delete mode 100644 packages/kit/src/utils/logger-client.ts delete mode 100644 packages/kit/src/utils/logger-node.ts delete mode 100644 packages/kit/src/utils/logger-types.ts delete mode 100644 packages/kit/src/utils/logger.test.ts diff --git a/alias.ts b/alias.ts index 4f3b2b7d..ada1281e 100644 --- a/alias.ts +++ b/alias.ts @@ -12,6 +12,7 @@ export const alias = { '@vitejs/devtools-rpc': r('rpc/src'), '@vitejs/devtools-kit/client': r('kit/src/client/index.ts'), '@vitejs/devtools-kit/utils/events': r('kit/src/utils/events.ts'), + '@vitejs/devtools-kit/utils/logger': r('kit/src/utils/logger.ts'), '@vitejs/devtools-kit/utils/nanoid': r('kit/src/utils/nanoid.ts'), '@vitejs/devtools-kit/utils/shared-state': r('kit/src/utils/shared-state.ts'), '@vitejs/devtools-kit': r('kit/src/index.ts'), diff --git a/packages/core/src/client/webcomponents/.generated/css.ts b/packages/core/src/client/webcomponents/.generated/css.ts index 8aa08097..23ac7251 100644 --- a/packages/core/src/client/webcomponents/.generated/css.ts +++ b/packages/core/src/client/webcomponents/.generated/css.ts @@ -1,3 +1,3 @@ /* eslint-disable eslint-comments/no-unlimited-disable */ /* eslint-disable */ -export default "*{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--un-default-border-color,#e5e7eb)}:before{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--un-default-border-color,#e5e7eb)}:after{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--un-default-border-color,#e5e7eb)}:before{--un-content:\"\"}:after{--un-content:\"\"}html{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button{-webkit-appearance:button;background-color:transparent;background-image:none}[type=button]{-webkit-appearance:button;background-color:transparent;background-image:none}[type=reset]{-webkit-appearance:button;background-color:transparent;background-image:none}[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::placeholder{opacity:1;color:#9ca3af}textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.xterm{cursor:text;user-select:none;position:relative}.xterm.focus{outline:none}.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;top:0;bottom:0;left:0;right:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-message{z-index:10;color:transparent;pointer-events:none;position:absolute;top:0;bottom:0;left:0;right:0}.xterm .xterm-accessibility:not(.debug){z-index:10;color:transparent;pointer-events:none;position:absolute;top:0;bottom:0;left:0;right:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:underline double}.xterm-underline-3{text-decoration:underline wavy}.xterm-underline-4{text-decoration:underline dotted}.xterm-underline-5{text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000)0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000)6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000)6px 0 6px -6px inset}:root{--un-text-opacity:100%}#vite-devtools-anchor{z-index:2147483644;box-sizing:border-box;transform-origin:50%;width:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:15px;position:fixed;transform:translate(-50%,-50%)rotate(0)}#vite-devtools-anchor #vite-devtools-dock-container{width:max-content;min-width:100px;height:40px;display:flex;position:absolute;top:0;left:0;transform:translate(-50%,-50%)}#vite-devtools-anchor.vite-devtools-vertical #vite-devtools-dock-container{transition-property:all;transition-duration:.5s;transition-timing-function:cubic-bezier(.4,0,.2,1);transform:translate(-50%,-50%)rotate(90deg)}#vite-devtools-anchor #vite-devtools-dock{touch-action:none;user-select:none;--vdt-backdrop-blur:blur(7px);height:40px;backdrop-filter:var(--vdt-backdrop-blur)var(--vdt-backdrop-brightness)var(--vdt-backdrop-contrast)var(--vdt-backdrop-grayscale)var(--vdt-backdrop-hue-rotate)var(--vdt-backdrop-invert)var(--vdt-backdrop-opacity)var(--vdt-backdrop-saturate)var(--vdt-backdrop-sepia);--vdt-text-opacity:1;color:rgba(51,51,51,var(--vdt-text-opacity));--vdt-shadow:var(--vdt-shadow-inset)0 1px 3px 0 var(--vdt-shadow-color,rgba(0,0,0,.1)),var(--vdt-shadow-inset)0 1px 2px -1px var(--vdt-shadow-color,rgba(0,0,0,.1));box-shadow:var(--vdt-ring-offset-shadow),var(--vdt-ring-shadow),var(--vdt-shadow);width:calc-size(max-content,size);background-color:rgba(255,255,255,.5);-webkit-border-radius:9999px;border-radius:9999px;margin:auto;transition-property:all;transition-duration:.5s;transition-timing-function:cubic-bezier(.4,0,.2,1)}@media (prefers-color-scheme:dark){#vite-devtools-anchor #vite-devtools-dock{--vdt-text-opacity:1;color:rgba(255,255,255,var(--vdt-text-opacity));background-color:rgba(17,17,17,.5)}}#vite-devtools-anchor.vite-devtools-minimized #vite-devtools-dock{width:22px;height:22px;padding:2px 0}#vite-devtools-anchor.vite-devtools-minimized .vite-devtools-dock-bracket{opacity:.5;width:.375rem}#vite-devtools-anchor:hover #vite-devtools-glowing{opacity:.6}#vite-devtools-anchor #vite-devtools-glowing{pointer-events:none;z-index:-1;opacity:0;--vdt-blur:blur(60px);width:160px;height:160px;filter:var(--vdt-blur)var(--vdt-brightness)var(--vdt-contrast)var(--vdt-drop-shadow)var(--vdt-grayscale)var(--vdt-hue-rotate)var(--vdt-invert)var(--vdt-saturate)var(--vdt-sepia);background-image:linear-gradient(45deg,#61d9ff,#7a23a1,#715ebd);-webkit-border-radius:9999px;border-radius:9999px;transition-property:all;transition-duration:1s;transition-timing-function:cubic-bezier(0,0,.2,1);position:absolute;top:0;left:0;transform:translate(-50%,-50%)}@media print{#vite-devtools-anchor{display:none}}.vite-devtools-resize-handle-horizontal{cursor:ns-resize;-webkit-border-radius:.375rem;border-radius:.375rem;height:10px;margin-top:-5px;margin-bottom:-5px;position:absolute;left:6px;right:6px}.vite-devtools-resize-handle-vertical{cursor:ew-resize;-webkit-border-radius:.375rem;border-radius:.375rem;width:10px;margin-left:-5px;margin-right:-5px;position:absolute;top:6px;bottom:0}.vite-devtools-resize-handle-corner{-webkit-border-radius:.375rem;border-radius:.375rem;width:14px;height:14px;margin:-6px;position:absolute}.vite-devtools-resize-handle{z-index:30}.vite-devtools-resize-handle:hover{background-color:rgba(156,163,175,.1)}*{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }:before{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }:after{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }::backdrop{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }.i-carbon-clean{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 32 32' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z'/%3E%3Cpath fill='currentColor' d='M17.003 20a4.9 4.9 0 0 0-2.404-4.173L22 3l-1.73-1l-7.577 13.126a5.7 5.7 0 0 0-5.243 1.503C3.706 20.24 3.996 28.682 4.01 29.04a1 1 0 0 0 1 .96h14.991a1 1 0 0 0 .6-1.8c-3.54-2.656-3.598-8.146-3.598-8.2m-5.073-3.003A3.11 3.11 0 0 1 15.004 20c0 .038.002.208.017.469l-5.9-2.624a3.8 3.8 0 0 1 2.809-.848M15.45 28A5.2 5.2 0 0 1 14 25h-2a6.5 6.5 0 0 0 .968 3h-2.223A16.6 16.6 0 0 1 10 24H8a17.3 17.3 0 0 0 .665 4H6c.031-1.836.29-5.892 1.803-8.553l7.533 3.35A13 13 0 0 0 17.596 28Z'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon)no-repeat;mask:var(--vdt-icon)no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-fluent-emoji-flat-warning{background:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 32 32' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='none'%3E%3Cpath fill='%23FFB02E' d='m14.839 5.668l-12.66 21.93c-.51.89.13 2.01 1.16 2.01h25.32c1.03 0 1.67-1.11 1.16-2.01l-12.66-21.93c-.52-.89-1.8-.89-2.32 0'/%3E%3Cpath fill='%23000' d='M14.599 21.498a1.4 1.4 0 1 0 2.8-.01v-9.16c0-.77-.62-1.4-1.4-1.4c-.77 0-1.4.62-1.4 1.4zm2.8 3.98a1.4 1.4 0 1 1-2.8 0a1.4 1.4 0 0 1 2.8 0'/%3E%3C/g%3E%3C/svg%3E\") 0 0/100% 100% no-repeat;width:1em;height:1em}.i-ph-check-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16' opacity='.2'/%3E%3Cpath d='m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon)no-repeat;mask:var(--vdt-icon)no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-rocket-launch-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 120v61.65a8 8 0 0 1-2.34 5.65l-34.35 34.35a8 8 0 0 1-13.57-4.53L128 176Zm-48-48H74.35a8 8 0 0 0-5.65 2.34l-34.35 34.35a8 8 0 0 0 4.53 13.57L80 128ZM40 216c37.65 0 50.69-19.69 54.56-28.18l-26.38-26.38C59.69 165.31 40 178.35 40 216' opacity='.2'/%3E%3Cpath d='M223.85 47.12a16 16 0 0 0-15-15c-12.58-.75-44.73.4-71.41 27.07L132.69 64H74.36A15.9 15.9 0 0 0 63 68.68L28.7 103a16 16 0 0 0 9.07 27.16l38.47 5.37l44.21 44.21l5.37 38.49a15.94 15.94 0 0 0 10.78 12.92a16.1 16.1 0 0 0 5.1.83a15.9 15.9 0 0 0 11.3-4.68l34.32-34.3a15.9 15.9 0 0 0 4.68-11.36v-58.33l4.77-4.77c26.68-26.68 27.83-58.83 27.08-71.42M74.36 80h42.33l-39.53 39.52L40 114.34Zm74.41-9.45a76.65 76.65 0 0 1 59.11-22.47a76.46 76.46 0 0 1-22.42 59.16L128 164.68L91.32 128ZM176 181.64L141.67 216l-5.19-37.17L176 139.31Zm-74.16 9.5C97.34 201 82.29 224 40 224a8 8 0 0 1-8-8c0-42.29 23-57.34 32.86-61.85a8 8 0 0 1 6.64 14.56c-6.43 2.93-20.62 12.36-23.12 38.91c26.55-2.5 36-16.69 38.91-23.12a8 8 0 1 1 14.56 6.64Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon)no-repeat;mask:var(--vdt-icon)no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-warning-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91' opacity='.2'/%3E%3Cpath d='M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon)no-repeat;mask:var(--vdt-icon)no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-svg-spinners-8-dots-rotate{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg%3E%3Ccircle cx='3' cy='12' r='2' fill='currentColor'/%3E%3Ccircle cx='21' cy='12' r='2' fill='currentColor'/%3E%3Ccircle cx='12' cy='21' r='2' fill='currentColor'/%3E%3Ccircle cx='12' cy='3' r='2' fill='currentColor'/%3E%3Ccircle cx='5.64' cy='5.64' r='2' fill='currentColor'/%3E%3Ccircle cx='18.36' cy='18.36' r='2' fill='currentColor'/%3E%3Ccircle cx='5.64' cy='18.36' r='2' fill='currentColor'/%3E%3Ccircle cx='18.36' cy='5.64' r='2' fill='currentColor'/%3E%3CanimateTransform attributeName='transform' dur='1.5s' repeatCount='indefinite' type='rotate' values='0 12 12;360 12 12'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon)no-repeat;mask:var(--vdt-icon)no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.container{width:100%}.z-floating-tooltip{z-index:2147483645}.border-base{--vdt-border-opacity:.13;border-color:rgba(136,136,136,var(--vdt-border-opacity))}.bg-active{--vdt-bg-opacity:.07;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}.bg-glass{--vdt-backdrop-blur:blur(7px);backdrop-filter:var(--vdt-backdrop-blur)var(--vdt-backdrop-brightness)var(--vdt-backdrop-contrast)var(--vdt-backdrop-grayscale)var(--vdt-backdrop-hue-rotate)var(--vdt-backdrop-invert)var(--vdt-backdrop-opacity)var(--vdt-backdrop-saturate)var(--vdt-backdrop-sepia);background-color:rgba(255,255,255,.5)}.bg-glass\\:75{--vdt-backdrop-blur:blur(7px);backdrop-filter:var(--vdt-backdrop-blur)var(--vdt-backdrop-brightness)var(--vdt-backdrop-contrast)var(--vdt-backdrop-grayscale)var(--vdt-backdrop-hue-rotate)var(--vdt-backdrop-invert)var(--vdt-backdrop-opacity)var(--vdt-backdrop-saturate)var(--vdt-backdrop-sepia);background-color:rgba(255,255,255,.75)}.hover\\:bg-active:hover{--vdt-bg-opacity:.07;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}@media (prefers-color-scheme:dark){.bg-glass{background-color:rgba(17,17,17,.5)}.bg-glass\\:75{background-color:rgba(17,17,17,.75)}}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.disabled\\:pointer-events-none:disabled{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{top:0;bottom:0;left:0;right:0}.bottom-0{bottom:0}.left--1{left:-.25rem}.left-1\\/2{left:50%}.right--1{right:-.25rem}.right--1px{right:-1px}.right-0{right:0}.top-0\\.5{top:.125rem}.top-1\\/2{top:50%}.z--1{z-index:-1}.grid{display:grid}.grid-rows-\\[max-content_1fr\\]{grid-template-rows:max-content 1fr}.m-auto{margin:auto}.m1{margin:.25rem}.mb2{margin-bottom:.5rem}.mt8{margin-top:2rem}.h-1\\.5{height:.375rem}.h-10{height:2.5rem}.h-20{height:5rem}.h-20px{height:20px}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-full{height:100%}.max-w-150{max-width:37.5rem}.w-1\\.5{width:.375rem}.w-10{width:2.5rem}.w-2\\.5{width:.625rem}.w-20{width:5rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.w-max{width:max-content}.w-px{width:1px}.flex{display:flex}.flex-none{flex:none}.flex-col{flex-direction:column}.translate-x--1\\/2{--vdt-translate-x:-50%;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.translate-y--1\\/2{--vdt-translate-y:-50%;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.rotate-0{--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-rotate:0deg;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.rotate-270{--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-rotate:270deg;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.scale-120{--vdt-scale-x:1.2;--vdt-scale-y:1.2;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.hover\\:scale-110:hover{--vdt-scale-x:1.1;--vdt-scale-y:1.1;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.scale-y--100{--vdt-scale-y:-1;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.transform{transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.select-none{user-select:none}.resize{resize:both}.items-center{align-items:center}.justify-center{justify-content:center}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.of-auto{overflow:auto}.of-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.ws-nowrap{white-space:nowrap}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-r-1\\.5{border-right-width:1.5px}.rounded{-webkit-border-radius:.25rem;border-radius:.25rem}.rounded-full{-webkit-border-radius:9999px;border-radius:9999px}.rounded-lg{-webkit-border-radius:.5rem;border-radius:.5rem}.rounded-xl{-webkit-border-radius:.75rem;border-radius:.75rem}.rounded-t{-webkit-border-top-left-radius:.25rem;border-top-left-radius:.25rem;-webkit-border-top-right-radius:.25rem;border-top-right-radius:.25rem}.bg-black{--vdt-bg-opacity:1;background-color:rgba(0,0,0,var(--vdt-bg-opacity))}.bg-gray-6{--vdt-bg-opacity:1;background-color:rgba(75,85,99,var(--vdt-bg-opacity))}.bg-green\\:5{background-color:rgba(74,222,128,.05)}.bg-lime6{--vdt-bg-opacity:1;background-color:rgba(101,163,13,var(--vdt-bg-opacity))}.hover\\:bg-\\[\\#8881\\]:hover{--vdt-bg-opacity:.07;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}.hover\\:bg-lime7:hover{--vdt-bg-opacity:1;background-color:rgba(77,124,15,var(--vdt-bg-opacity))}.disabled\\:bg-gray6\\!:disabled{--vdt-bg-opacity:1!important;background-color:rgba(75,85,99,var(--vdt-bg-opacity))!important}.fill-black{--vdt-fill-opacity:1;fill:rgba(0,0,0,var(--vdt-fill-opacity))}.fill-hex-08060D{--vdt-fill-opacity:1;fill:rgba(8,6,13,var(--vdt-fill-opacity))}.p1{padding:.25rem}.p1\\.5{padding:.375rem}.p2{padding:.5rem}.p20{padding:5rem}.px,.px4{padding-left:1rem;padding-right:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px2{padding-left:.5rem;padding-right:.5rem}.px3{padding-left:.75rem;padding-right:.75rem}.py1{padding-top:.25rem;padding-bottom:.25rem}.py1\\.5{padding-top:.375rem;padding-bottom:.375rem}.text-center{text-align:center}.text-0\\.6em{font-size:.6em}.text-2xl{font-size:1.5rem;line-height:2rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-amber{--vdt-text-opacity:1;color:rgba(251,191,36,var(--vdt-text-opacity))}.text-green{--vdt-text-opacity:1;color:rgba(74,222,128,var(--vdt-text-opacity))}.text-orange{--vdt-text-opacity:1;color:rgba(251,146,60,var(--vdt-text-opacity))}.text-purple{--vdt-text-opacity:1;color:rgba(192,132,252,var(--vdt-text-opacity))}.text-violet{--vdt-text-opacity:1;color:rgba(167,139,250,var(--vdt-text-opacity))}.text-white{--vdt-text-opacity:1;color:rgba(255,255,255,var(--vdt-text-opacity))}.font-bold{font-weight:700}.tab{tab-size:4}.op0,.opacity-0{opacity:0}.op100,.opacity-100{opacity:1}.op50{opacity:.5}.op60\\!{opacity:.6!important}.op75{opacity:.75}.shadow{--vdt-shadow:var(--vdt-shadow-inset)0 1px 3px 0 var(--vdt-shadow-color,rgba(0,0,0,.1)),var(--vdt-shadow-inset)0 1px 2px -1px var(--vdt-shadow-color,rgba(0,0,0,.1));box-shadow:var(--vdt-ring-offset-shadow),var(--vdt-ring-shadow),var(--vdt-shadow)}.saturate-0{--vdt-saturate:saturate(0);filter:var(--vdt-blur)var(--vdt-brightness)var(--vdt-contrast)var(--vdt-drop-shadow)var(--vdt-grayscale)var(--vdt-hue-rotate)var(--vdt-invert)var(--vdt-saturate)var(--vdt-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-property:all;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.delay-200{transition-delay:.2s}@media (prefers-color-scheme:dark){.dark-hidden{display:none}.dark\\:fill-hex-fff,.dark\\:fill-white{--vdt-fill-opacity:1;fill:rgba(255,255,255,var(--vdt-fill-opacity))}}@media (prefers-color-scheme:light){.light-hidden{display:none}}" +export default "*{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--un-default-border-color,#e5e7eb)}:before{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--un-default-border-color,#e5e7eb)}:after{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--un-default-border-color,#e5e7eb)}:before{--un-content:\"\"}:after{--un-content:\"\"}html{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button{-webkit-appearance:button;background-color:transparent;background-image:none}[type=button]{-webkit-appearance:button;background-color:transparent;background-image:none}[type=reset]{-webkit-appearance:button;background-color:transparent;background-image:none}[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::placeholder{opacity:1;color:#9ca3af}textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.xterm{cursor:text;user-select:none;position:relative}.xterm.focus{outline:none}.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;top:0;bottom:0;left:0;right:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-message{z-index:10;color:transparent;pointer-events:none;position:absolute;top:0;bottom:0;left:0;right:0}.xterm .xterm-accessibility:not(.debug){z-index:10;color:transparent;pointer-events:none;position:absolute;top:0;bottom:0;left:0;right:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:underline double}.xterm-underline-3{text-decoration:underline wavy}.xterm-underline-4{text-decoration:underline dotted}.xterm-underline-5{text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000)0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000)6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000)6px 0 6px -6px inset}:root{--un-text-opacity:100%}#vite-devtools-anchor{z-index:2147483644;box-sizing:border-box;transform-origin:50%;width:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:15px;position:fixed;transform:translate(-50%,-50%)rotate(0)}#vite-devtools-anchor #vite-devtools-dock-container{width:max-content;min-width:100px;height:40px;display:flex;position:absolute;top:0;left:0;transform:translate(-50%,-50%)}#vite-devtools-anchor.vite-devtools-vertical #vite-devtools-dock-container{transition-property:all;transition-duration:.5s;transition-timing-function:cubic-bezier(.4,0,.2,1);transform:translate(-50%,-50%)rotate(90deg)}#vite-devtools-anchor #vite-devtools-dock{touch-action:none;user-select:none;--vdt-backdrop-blur:blur(7px);height:40px;backdrop-filter:var(--vdt-backdrop-blur)var(--vdt-backdrop-brightness)var(--vdt-backdrop-contrast)var(--vdt-backdrop-grayscale)var(--vdt-backdrop-hue-rotate)var(--vdt-backdrop-invert)var(--vdt-backdrop-opacity)var(--vdt-backdrop-saturate)var(--vdt-backdrop-sepia);--vdt-text-opacity:1;color:rgba(51,51,51,var(--vdt-text-opacity));--vdt-shadow:var(--vdt-shadow-inset)0 1px 3px 0 var(--vdt-shadow-color,rgba(0,0,0,.1)),var(--vdt-shadow-inset)0 1px 2px -1px var(--vdt-shadow-color,rgba(0,0,0,.1));box-shadow:var(--vdt-ring-offset-shadow),var(--vdt-ring-shadow),var(--vdt-shadow);width:calc-size(max-content,size);background-color:rgba(255,255,255,.5);-webkit-border-radius:9999px;border-radius:9999px;margin:auto;transition-property:all;transition-duration:.5s;transition-timing-function:cubic-bezier(.4,0,.2,1)}@media (prefers-color-scheme:dark){#vite-devtools-anchor #vite-devtools-dock{--vdt-text-opacity:1;color:rgba(255,255,255,var(--vdt-text-opacity));background-color:rgba(17,17,17,.5)}}#vite-devtools-anchor.vite-devtools-minimized #vite-devtools-dock{width:22px;height:22px;padding:2px 0}#vite-devtools-anchor.vite-devtools-minimized .vite-devtools-dock-bracket{opacity:.5;width:.375rem}#vite-devtools-anchor:hover #vite-devtools-glowing{opacity:.6}#vite-devtools-anchor #vite-devtools-glowing{pointer-events:none;z-index:-1;opacity:0;--vdt-blur:blur(60px);width:160px;height:160px;filter:var(--vdt-blur)var(--vdt-brightness)var(--vdt-contrast)var(--vdt-drop-shadow)var(--vdt-grayscale)var(--vdt-hue-rotate)var(--vdt-invert)var(--vdt-saturate)var(--vdt-sepia);background-image:linear-gradient(45deg,#61d9ff,#7a23a1,#715ebd);-webkit-border-radius:9999px;border-radius:9999px;transition-property:all;transition-duration:1s;transition-timing-function:cubic-bezier(0,0,.2,1);position:absolute;top:0;left:0;transform:translate(-50%,-50%)}@media print{#vite-devtools-anchor{display:none}}.vite-devtools-resize-handle-horizontal{cursor:ns-resize;-webkit-border-radius:.375rem;border-radius:.375rem;height:10px;margin-top:-5px;margin-bottom:-5px;position:absolute;left:6px;right:6px}.vite-devtools-resize-handle-vertical{cursor:ew-resize;-webkit-border-radius:.375rem;border-radius:.375rem;width:10px;margin-left:-5px;margin-right:-5px;position:absolute;top:6px;bottom:0}.vite-devtools-resize-handle-corner{-webkit-border-radius:.375rem;border-radius:.375rem;width:14px;height:14px;margin:-6px;position:absolute}.vite-devtools-resize-handle{z-index:30}.vite-devtools-resize-handle:hover{background-color:rgba(156,163,175,.1)}*{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }:before{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }:after{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }::backdrop{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }.i-ph-check-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16' opacity='.2'/%3E%3Cpath d='m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon)no-repeat;mask:var(--vdt-icon)no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-rocket-launch-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 120v61.65a8 8 0 0 1-2.34 5.65l-34.35 34.35a8 8 0 0 1-13.57-4.53L128 176Zm-48-48H74.35a8 8 0 0 0-5.65 2.34l-34.35 34.35a8 8 0 0 0 4.53 13.57L80 128ZM40 216c37.65 0 50.69-19.69 54.56-28.18l-26.38-26.38C59.69 165.31 40 178.35 40 216' opacity='.2'/%3E%3Cpath d='M223.85 47.12a16 16 0 0 0-15-15c-12.58-.75-44.73.4-71.41 27.07L132.69 64H74.36A15.9 15.9 0 0 0 63 68.68L28.7 103a16 16 0 0 0 9.07 27.16l38.47 5.37l44.21 44.21l5.37 38.49a15.94 15.94 0 0 0 10.78 12.92a16.1 16.1 0 0 0 5.1.83a15.9 15.9 0 0 0 11.3-4.68l34.32-34.3a15.9 15.9 0 0 0 4.68-11.36v-58.33l4.77-4.77c26.68-26.68 27.83-58.83 27.08-71.42M74.36 80h42.33l-39.53 39.52L40 114.34Zm74.41-9.45a76.65 76.65 0 0 1 59.11-22.47a76.46 76.46 0 0 1-22.42 59.16L128 164.68L91.32 128ZM176 181.64L141.67 216l-5.19-37.17L176 139.31Zm-74.16 9.5C97.34 201 82.29 224 40 224a8 8 0 0 1-8-8c0-42.29 23-57.34 32.86-61.85a8 8 0 0 1 6.64 14.56c-6.43 2.93-20.62 12.36-23.12 38.91c26.55-2.5 36-16.69 38.91-23.12a8 8 0 1 1 14.56 6.64Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon)no-repeat;mask:var(--vdt-icon)no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-warning-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91' opacity='.2'/%3E%3Cpath d='M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon)no-repeat;mask:var(--vdt-icon)no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.container{width:100%}.z-floating-tooltip{z-index:2147483645}.border-base{--vdt-border-opacity:.13;border-color:rgba(136,136,136,var(--vdt-border-opacity))}.bg-active{--vdt-bg-opacity:.07;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}.bg-glass{--vdt-backdrop-blur:blur(7px);backdrop-filter:var(--vdt-backdrop-blur)var(--vdt-backdrop-brightness)var(--vdt-backdrop-contrast)var(--vdt-backdrop-grayscale)var(--vdt-backdrop-hue-rotate)var(--vdt-backdrop-invert)var(--vdt-backdrop-opacity)var(--vdt-backdrop-saturate)var(--vdt-backdrop-sepia);background-color:rgba(255,255,255,.5)}.bg-glass\\:75{--vdt-backdrop-blur:blur(7px);backdrop-filter:var(--vdt-backdrop-blur)var(--vdt-backdrop-brightness)var(--vdt-backdrop-contrast)var(--vdt-backdrop-grayscale)var(--vdt-backdrop-hue-rotate)var(--vdt-backdrop-invert)var(--vdt-backdrop-opacity)var(--vdt-backdrop-saturate)var(--vdt-backdrop-sepia);background-color:rgba(255,255,255,.75)}.hover\\:bg-active:hover{--vdt-bg-opacity:.07;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}@media (prefers-color-scheme:dark){.bg-glass{background-color:rgba(17,17,17,.5)}.bg-glass\\:75{background-color:rgba(17,17,17,.75)}}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.disabled\\:pointer-events-none:disabled{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{top:0;bottom:0;left:0;right:0}.bottom-0{bottom:0}.left--1{left:-.25rem}.left-1\\/2{left:50%}.right--1{right:-.25rem}.right--1px{right:-1px}.right-0{right:0}.top-0\\.5{top:.125rem}.top-1\\/2{top:50%}.z--1{z-index:-1}.grid{display:grid}.grid-rows-\\[max-content_1fr\\]{grid-template-rows:max-content 1fr}.m-auto{margin:auto}.m1{margin:.25rem}.mb2{margin-bottom:.5rem}.mt8{margin-top:2rem}.h-1\\.5{height:.375rem}.h-10{height:2.5rem}.h-20{height:5rem}.h-20px{height:20px}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-full{height:100%}.max-w-150{max-width:37.5rem}.w-1\\.5{width:.375rem}.w-10{width:2.5rem}.w-2\\.5{width:.625rem}.w-20{width:5rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.w-max{width:max-content}.w-px{width:1px}.flex{display:flex}.flex-none{flex:none}.flex-col{flex-direction:column}.translate-x--1\\/2{--vdt-translate-x:-50%;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.translate-y--1\\/2{--vdt-translate-y:-50%;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.rotate-0{--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-rotate:0deg;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.rotate-270{--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-rotate:270deg;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.scale-120{--vdt-scale-x:1.2;--vdt-scale-y:1.2;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.hover\\:scale-110:hover{--vdt-scale-x:1.1;--vdt-scale-y:1.1;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.scale-y--100{--vdt-scale-y:-1;transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.transform{transform:translateX(var(--vdt-translate-x))translateY(var(--vdt-translate-y))translateZ(var(--vdt-translate-z))rotate(var(--vdt-rotate))rotateX(var(--vdt-rotate-x))rotateY(var(--vdt-rotate-y))rotateZ(var(--vdt-rotate-z))skewX(var(--vdt-skew-x))skewY(var(--vdt-skew-y))scaleX(var(--vdt-scale-x))scaleY(var(--vdt-scale-y))scaleZ(var(--vdt-scale-z))}.select-none{user-select:none}.resize{resize:both}.items-center{align-items:center}.justify-center{justify-content:center}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.of-auto{overflow:auto}.of-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.ws-nowrap{white-space:nowrap}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-r-1\\.5{border-right-width:1.5px}.rounded{-webkit-border-radius:.25rem;border-radius:.25rem}.rounded-full{-webkit-border-radius:9999px;border-radius:9999px}.rounded-lg{-webkit-border-radius:.5rem;border-radius:.5rem}.rounded-xl{-webkit-border-radius:.75rem;border-radius:.75rem}.rounded-t{-webkit-border-top-left-radius:.25rem;border-top-left-radius:.25rem;-webkit-border-top-right-radius:.25rem;border-top-right-radius:.25rem}.bg-black{--vdt-bg-opacity:1;background-color:rgba(0,0,0,var(--vdt-bg-opacity))}.bg-gray-6{--vdt-bg-opacity:1;background-color:rgba(75,85,99,var(--vdt-bg-opacity))}.bg-green\\:5{background-color:rgba(74,222,128,.05)}.bg-lime6{--vdt-bg-opacity:1;background-color:rgba(101,163,13,var(--vdt-bg-opacity))}.hover\\:bg-\\[\\#8881\\]:hover{--vdt-bg-opacity:.07;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}.hover\\:bg-lime7:hover{--vdt-bg-opacity:1;background-color:rgba(77,124,15,var(--vdt-bg-opacity))}.disabled\\:bg-gray6\\!:disabled{--vdt-bg-opacity:1!important;background-color:rgba(75,85,99,var(--vdt-bg-opacity))!important}.fill-black{--vdt-fill-opacity:1;fill:rgba(0,0,0,var(--vdt-fill-opacity))}.fill-hex-08060D{--vdt-fill-opacity:1;fill:rgba(8,6,13,var(--vdt-fill-opacity))}.p1{padding:.25rem}.p1\\.5{padding:.375rem}.p2{padding:.5rem}.p20{padding:5rem}.px,.px4{padding-left:1rem;padding-right:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px2{padding-left:.5rem;padding-right:.5rem}.px3{padding-left:.75rem;padding-right:.75rem}.py1{padding-top:.25rem;padding-bottom:.25rem}.py1\\.5{padding-top:.375rem;padding-bottom:.375rem}.text-center{text-align:center}.text-0\\.6em{font-size:.6em}.text-2xl{font-size:1.5rem;line-height:2rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-amber{--vdt-text-opacity:1;color:rgba(251,191,36,var(--vdt-text-opacity))}.text-green{--vdt-text-opacity:1;color:rgba(74,222,128,var(--vdt-text-opacity))}.text-orange{--vdt-text-opacity:1;color:rgba(251,146,60,var(--vdt-text-opacity))}.text-purple{--vdt-text-opacity:1;color:rgba(192,132,252,var(--vdt-text-opacity))}.text-violet{--vdt-text-opacity:1;color:rgba(167,139,250,var(--vdt-text-opacity))}.text-white{--vdt-text-opacity:1;color:rgba(255,255,255,var(--vdt-text-opacity))}.font-bold{font-weight:700}.tab{tab-size:4}.op0,.opacity-0{opacity:0}.op100,.opacity-100{opacity:1}.op50{opacity:.5}.op60\\!{opacity:.6!important}.op75{opacity:.75}.shadow{--vdt-shadow:var(--vdt-shadow-inset)0 1px 3px 0 var(--vdt-shadow-color,rgba(0,0,0,.1)),var(--vdt-shadow-inset)0 1px 2px -1px var(--vdt-shadow-color,rgba(0,0,0,.1));box-shadow:var(--vdt-ring-offset-shadow),var(--vdt-ring-shadow),var(--vdt-shadow)}.saturate-0{--vdt-saturate:saturate(0);filter:var(--vdt-blur)var(--vdt-brightness)var(--vdt-contrast)var(--vdt-drop-shadow)var(--vdt-grayscale)var(--vdt-hue-rotate)var(--vdt-invert)var(--vdt-saturate)var(--vdt-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-property:all;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.delay-200{transition-delay:.2s}@media (prefers-color-scheme:dark){.dark-hidden{display:none}.dark\\:fill-hex-fff,.dark\\:fill-white{--vdt-fill-opacity:1;fill:rgba(255,255,255,var(--vdt-fill-opacity))}}@media (prefers-color-scheme:light){.light-hidden{display:none}}" diff --git a/packages/core/src/client/webcomponents/components/DockEntriesWithCategories.vue b/packages/core/src/client/webcomponents/components/DockEntriesWithCategories.vue index 18afdcb4..bfaa27b1 100644 --- a/packages/core/src/client/webcomponents/components/DockEntriesWithCategories.vue +++ b/packages/core/src/client/webcomponents/components/DockEntriesWithCategories.vue @@ -61,7 +61,7 @@ const groups = computed(() => { left = 0 } else { - left -= entries.length + left -= items.length visible.push([category, items]) } } diff --git a/packages/core/src/node/context.ts b/packages/core/src/node/context.ts index 46043764..b3f1dc15 100644 --- a/packages/core/src/node/context.ts +++ b/packages/core/src/node/context.ts @@ -1,5 +1,6 @@ import type { DevToolsNodeContext } from '@vitejs/devtools-kit' import type { ResolvedConfig, ViteDevServer } from 'vite' +import { createLogger } from '@vitejs/devtools-kit/utils/logger' import { createDebug } from 'obug' import { debounce } from 'perfect-debounce' import { searchForWorkspaceRoot } from 'vite' @@ -29,6 +30,7 @@ export async function createDevToolsContext( views: undefined!, utils: ContextUtils, terminals: undefined!, + logger: createLogger('vite-devtools'), } const rpcHost = new RpcFunctionsHost(context) const docksHost = new DevToolsDockHost(context) diff --git a/packages/kit/package.json b/packages/kit/package.json index d3e6a47a..0427d467 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -22,11 +22,9 @@ ".": "./dist/index.mjs", "./client": "./dist/client.mjs", "./utils/events": "./dist/utils/events.mjs", + "./utils/logger": "./dist/utils/logger.mjs", "./utils/nanoid": "./dist/utils/nanoid.mjs", "./utils/shared-state": "./dist/utils/shared-state.mjs", - "./utils/logger": "./dist/utils/logger.mjs", - "./utils/logger-node": "./dist/utils/logger-node.mjs", - "./utils/logger-client": "./dist/utils/logger-client.mjs", "./package.json": "./package.json" }, "types": "./dist/index.d.mts", @@ -45,6 +43,7 @@ "@vitejs/devtools-rpc": "workspace:*", "birpc": "catalog:deps", "birpc-x": "catalog:deps", + "consola": "catalog:deps", "immer": "catalog:deps" }, "devDependencies": { diff --git a/packages/kit/src/types/vite-plugin.ts b/packages/kit/src/types/vite-plugin.ts index e56ba7b3..c8ad35c7 100644 --- a/packages/kit/src/types/vite-plugin.ts +++ b/packages/kit/src/types/vite-plugin.ts @@ -1,3 +1,4 @@ +import type { ConsolaInstance } from 'consola' import type { ResolvedConfig, ViteDevServer } from 'vite' import type { DockClientScriptContext } from '../client' import type { ClientScriptEntry, DevToolsDockHost } from './docks' @@ -61,6 +62,23 @@ export interface DevToolsNodeContext { * Terminals host, for registering terminal sessions and streaming terminal output */ terminals: DevToolsTerminalHost + /** + * Logger instance for logging messages. + * Uses consola under the hood, similar to Nuxt Kit's logging API. + * + * @example + * ```ts + * context.logger.info('Plugin initialized') + * context.logger.debug('Debug info') + * context.logger.warn('Warning message') + * context.logger.error(new Error('Error occurred')) + * + * // Create child logger with tag + * const rpcLogger = context.logger.withTag('rpc') + * rpcLogger.info('RPC connected') + * ``` + */ + logger: ConsolaInstance } export interface DevToolsNodeUtils { diff --git a/packages/kit/src/utils/log-collector.ts b/packages/kit/src/utils/log-collector.ts deleted file mode 100644 index 076c74eb..00000000 --- a/packages/kit/src/utils/log-collector.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Log Collector - * - * Collects and stores log entries for display in the DevTools Logs panel. - * Supports filtering, subscribing to updates, and auto-pruning old entries. - */ - -import type { LogCollector, LogEntry, LogFilter, LogLevel } from './logger-types' - -const LOG_LEVEL_PRIORITY: Record = { - debug: 0, - info: 1, - warn: 2, - error: 3, - silent: 4, -} - -export interface LogCollectorOptions { - /** Maximum number of entries to keep. Default: 1000 */ - maxEntries?: number -} - -export function createLogCollector(options: LogCollectorOptions = {}): LogCollector { - const { maxEntries = 1000 } = options - - const entries: LogEntry[] = [] - const subscribers = new Set<(entries: readonly LogEntry[]) => void>() - - function notify(): void { - const snapshot = [...entries] as readonly LogEntry[] - subscribers.forEach(cb => cb(snapshot)) - } - - const collector: LogCollector = { - get entries(): readonly LogEntry[] { - return entries - }, - - maxEntries, - - add(entry: LogEntry): void { - entries.push(entry) - - // Prune old entries if over limit - while (entries.length > maxEntries) { - entries.shift() - } - - notify() - }, - - clear(): void { - entries.length = 0 - notify() - }, - - getEntries(filter?: LogFilter): LogEntry[] { - if (!filter) { - return [...entries] - } - - return entries.filter((entry) => { - // Filter by level (entry level must be >= filter level) - if (filter.level) { - const filterPriority = LOG_LEVEL_PRIORITY[filter.level] - const entryPriority = LOG_LEVEL_PRIORITY[entry.level] - if (entryPriority < filterPriority) { - return false - } - } - - // Filter by scope (partial match) - if (filter.scope && entry.scope) { - if (!entry.scope.includes(filter.scope)) { - return false - } - } - - // Filter by timestamp - if (filter.since && entry.timestamp < filter.since) { - return false - } - - return true - }) - }, - - subscribe(callback: (entries: readonly LogEntry[]) => void): () => void { - subscribers.add(callback) - - // Immediately call with current entries - callback([...entries] as readonly LogEntry[]) - - // Return unsubscribe function - return () => { - subscribers.delete(callback) - } - }, - } - - return collector -} diff --git a/packages/kit/src/utils/logger-client.ts b/packages/kit/src/utils/logger-client.ts deleted file mode 100644 index 7b5e82bb..00000000 --- a/packages/kit/src/utils/logger-client.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Logger Implementation for Browser/Client - * - * A lightweight, scoped logger for client-side Vite DevTools code. - * Supports styled console output, log levels, and log aggregation. - */ - -import type { LogEntry, Logger, LoggerOptions, LogLevel } from './logger-types' - -const LOG_LEVEL_PRIORITY: Record = { - debug: 0, - info: 1, - warn: 2, - error: 3, - silent: 4, -} - -const LEVEL_STYLES: Record, string> = { - debug: 'color: #9ca3af; font-weight: normal', - info: 'color: #3b82f6; font-weight: normal', - warn: 'color: #f59e0b; font-weight: bold', - error: 'color: #ef4444; font-weight: bold', -} - -const LEVEL_ICONS: Record, string> = { - debug: '🔍', - info: 'ℹ️', - warn: '⚠️', - error: '❌', -} - -export function createClientLogger(options: LoggerOptions = {}): Logger { - const { - level = 'info', - scope, - timestamps = false, - onLog, - } = options - - let currentLevel = level - - function shouldLog(msgLevel: LogLevel): boolean { - return LOG_LEVEL_PRIORITY[msgLevel] >= LOG_LEVEL_PRIORITY[currentLevel] - } - - function log(entry: LogEntry): void { - // Always call onLog for aggregation, regardless of level - onLog?.(entry) - - if (!shouldLog(entry.level) || entry.level === 'silent') { - return - } - - const style = LEVEL_STYLES[entry.level] - const icon = LEVEL_ICONS[entry.level] - const scopeText = entry.scope ? `[${entry.scope}]` : '[vite-devtools]' - const time = timestamps ? `${new Date(entry.timestamp).toISOString()} ` : '' - - // Build the formatted message - const prefix = `${time}${icon} ${scopeText}` - - const method = entry.level === 'debug' ? 'log' : entry.level - - // Use styled console output - // eslint-disable-next-line no-console - console[method]( - `%c${prefix}%c ${entry.message}`, - style, - 'color: inherit; font-weight: normal', - ) - - // Log error stack if present - if (entry.error?.stack) { - console.error(entry.error) - } - - // Log metadata if present - if (entry.meta && Object.keys(entry.meta).length > 0) { - // eslint-disable-next-line no-console - console.log(' ↳', entry.meta) - } - } - - function createLogMethod(level: Exclude) { - return (message: string | Error, meta?: Record) => { - const isError = message instanceof Error - const entry: LogEntry = { - level, - message: isError ? message.message : message, - timestamp: Date.now(), - scope, - meta, - error: isError ? message : undefined, - } - log(entry) - } - } - - const logger: Logger = { - debug: createLogMethod('debug'), - info: createLogMethod('info'), - warn: createLogMethod('warn'), - error: createLogMethod('error'), - - child(childScope: string): Logger { - const newScope = scope ? `${scope}:${childScope}` : childScope - return createClientLogger({ - level: currentLevel, - scope: newScope, - timestamps, - onLog, - }) - }, - - setLevel(newLevel: LogLevel) { - currentLevel = newLevel - }, - - getLevel(): LogLevel { - return currentLevel - }, - } - - return logger -} - -/** - * Default logger instance for convenience. - * Use `createClientLogger()` for custom configuration. - */ -export const logger = createClientLogger({ scope: 'vite-devtools' }) diff --git a/packages/kit/src/utils/logger-node.ts b/packages/kit/src/utils/logger-node.ts deleted file mode 100644 index d39fe1c3..00000000 --- a/packages/kit/src/utils/logger-node.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Logger Implementation for Node.js - * - * A lightweight, scoped logger for server-side Vite DevTools code. - * Supports colored output, log levels, and log aggregation. - */ - -import type { LogEntry, Logger, LoggerOptions, LogLevel } from './logger-types' -import process from 'node:process' - -const LOG_LEVEL_PRIORITY: Record = { - debug: 0, - info: 1, - warn: 2, - error: 3, - silent: 4, -} - -// ANSI color codes for terminal output -const COLORS = { - reset: '\x1B[0m', - gray: '\x1B[90m', - cyan: '\x1B[36m', - blue: '\x1B[34m', - yellow: '\x1B[33m', - red: '\x1B[31m', - bold: '\x1B[1m', -} as const - -const LEVEL_CONFIG: Record, { icon: string, color: string }> = { - debug: { icon: '🔍', color: COLORS.gray }, - info: { icon: 'ℹ', color: COLORS.blue }, - warn: { icon: '⚠', color: COLORS.yellow }, - error: { icon: '✖', color: COLORS.red }, -} - -function colorize(text: string, color: string): string { - return `${color}${text}${COLORS.reset}` -} - -function getEnvLogLevel(): LogLevel | undefined { - const envLevel = process.env.VITE_DEVTOOLS_LOG_LEVEL?.toLowerCase() - if (envLevel && envLevel in LOG_LEVEL_PRIORITY) { - return envLevel as LogLevel - } - return undefined -} - -export function createNodeLogger(options: LoggerOptions = {}): Logger { - const { - level = getEnvLogLevel() ?? 'info', - scope, - timestamps = false, - onLog, - } = options - - let currentLevel = level - - function shouldLog(msgLevel: LogLevel): boolean { - return LOG_LEVEL_PRIORITY[msgLevel] >= LOG_LEVEL_PRIORITY[currentLevel] - } - - function formatTimestamp(): string { - return colorize(new Date().toISOString(), COLORS.gray) - } - - function formatScope(s: string): string { - return colorize(`[${s}]`, COLORS.cyan) - } - - function log(entry: LogEntry): void { - // Always call onLog for aggregation, regardless of level - onLog?.(entry) - - if (!shouldLog(entry.level) || entry.level === 'silent') { - return - } - - const config = LEVEL_CONFIG[entry.level] - const parts: string[] = [] - - // Timestamp (optional) - if (timestamps) { - parts.push(formatTimestamp()) - } - - // Icon with color - parts.push(colorize(config.icon, config.color)) - - // Scope - if (entry.scope) { - parts.push(formatScope(entry.scope)) - } - - // Message - parts.push(entry.message) - - // Output - const output = parts.join(' ') - const method = entry.level === 'debug' ? 'log' : entry.level - // eslint-disable-next-line no-console - console[method](output) - - // Error stack trace - if (entry.error?.stack) { - console.error(colorize(entry.error.stack, COLORS.red)) - } - - // Metadata - if (entry.meta && Object.keys(entry.meta).length > 0) { - // eslint-disable-next-line no-console - console.log(colorize(' ↳', COLORS.gray), entry.meta) - } - } - - function createLogMethod(level: Exclude) { - return (message: string | Error, meta?: Record) => { - const isError = message instanceof Error - const entry: LogEntry = { - level, - message: isError ? message.message : message, - timestamp: Date.now(), - scope, - meta, - error: isError ? message : undefined, - } - log(entry) - } - } - - const logger: Logger = { - debug: createLogMethod('debug'), - info: createLogMethod('info'), - warn: createLogMethod('warn'), - error: createLogMethod('error'), - - child(childScope: string): Logger { - const newScope = scope ? `${scope}:${childScope}` : childScope - return createNodeLogger({ - level: currentLevel, - scope: newScope, - timestamps, - onLog, - }) - }, - - setLevel(newLevel: LogLevel) { - currentLevel = newLevel - }, - - getLevel(): LogLevel { - return currentLevel - }, - } - - return logger -} - -/** - * Default logger instance for convenience. - * Use `createNodeLogger()` for custom configuration. - */ -export const logger = createNodeLogger({ scope: 'vite-devtools' }) diff --git a/packages/kit/src/utils/logger-types.ts b/packages/kit/src/utils/logger-types.ts deleted file mode 100644 index 210ae4eb..00000000 --- a/packages/kit/src/utils/logger-types.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Logger Types - * - * Shared type definitions for the unified logging API. - */ - -export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent' - -export interface LogEntry { - /** Log level */ - level: LogLevel - /** Log message */ - message: string - /** Unix timestamp in milliseconds */ - timestamp: number - /** Logger scope/namespace (e.g., 'rpc', 'ws', 'client') */ - scope?: string - /** Additional structured metadata */ - meta?: Record - /** Error object if this is an error log */ - error?: Error -} - -export interface LoggerOptions { - /** Minimum log level to output. Default: 'info' */ - level?: LogLevel - /** Scope/namespace for the logger (e.g., 'rpc', 'ws', 'client') */ - scope?: string - /** Whether to include timestamps in output. Default: false */ - timestamps?: boolean - /** Custom log handler for aggregation/forwarding */ - onLog?: (entry: LogEntry) => void -} - -export interface Logger { - /** Log debug message (development only) */ - debug: (message: string, meta?: Record) => void - /** Log info message */ - info: (message: string, meta?: Record) => void - /** Log warning message */ - warn: (message: string, meta?: Record) => void - /** Log error message or Error object */ - error: (message: string | Error, meta?: Record) => void - - /** Create a child logger with a sub-scope */ - child: (scope: string) => Logger - - /** Update logger level at runtime */ - setLevel: (level: LogLevel) => void - - /** Get current log level */ - getLevel: () => LogLevel -} - -export interface LogCollector { - /** All collected log entries */ - readonly entries: readonly LogEntry[] - /** Maximum number of entries to keep */ - readonly maxEntries: number - - /** Add a log entry */ - add: (entry: LogEntry) => void - /** Clear all entries */ - clear: () => void - /** Get filtered entries */ - getEntries: (filter?: LogFilter) => LogEntry[] - - /** Subscribe to log updates */ - subscribe: (callback: (entries: readonly LogEntry[]) => void) => () => void -} - -export interface LogFilter { - /** Filter by log level */ - level?: LogLevel - /** Filter by scope (partial match) */ - scope?: string - /** Filter entries after this timestamp */ - since?: number -} diff --git a/packages/kit/src/utils/logger.test.ts b/packages/kit/src/utils/logger.test.ts deleted file mode 100644 index 5df4c270..00000000 --- a/packages/kit/src/utils/logger.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Logger Tests - */ - -import type { LogEntry } from './logger-types' -import { describe, expect, it, vi } from 'vitest' -import { createLogCollector } from './log-collector' -import { createNodeLogger } from './logger-node' - -describe('createNodeLogger', () => { - it('should create a logger with default options', () => { - const logger = createNodeLogger() - expect(logger).toBeDefined() - expect(logger.info).toBeInstanceOf(Function) - expect(logger.warn).toBeInstanceOf(Function) - expect(logger.error).toBeInstanceOf(Function) - expect(logger.debug).toBeInstanceOf(Function) - }) - - it('should respect log level', () => { - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const logger = createNodeLogger({ level: 'warn' }) - - logger.debug('debug message') - logger.info('info message') - - expect(consoleSpy).not.toHaveBeenCalled() - consoleSpy.mockRestore() - }) - - it('should call onLog callback for all levels', () => { - const entries: LogEntry[] = [] - const logger = createNodeLogger({ - level: 'silent', // Don't output to console - onLog: entry => entries.push(entry), - }) - - logger.debug('debug') - logger.info('info') - logger.warn('warn') - logger.error('error') - - expect(entries).toHaveLength(4) - expect(entries.map(e => e.level)).toEqual(['debug', 'info', 'warn', 'error']) - }) - - it('should create child logger with combined scope', () => { - const entries: LogEntry[] = [] - const logger = createNodeLogger({ - scope: 'parent', - level: 'silent', - onLog: entry => entries.push(entry), - }) - - const child = logger.child('child') - child.info('message') - - expect(entries[0].scope).toBe('parent:child') - }) - - it('should handle Error objects', () => { - const entries: LogEntry[] = [] - const logger = createNodeLogger({ - level: 'silent', - onLog: entry => entries.push(entry), - }) - - const error = new Error('test error') - logger.error(error) - - expect(entries[0].message).toBe('test error') - expect(entries[0].error).toBe(error) - }) - - it('should include metadata', () => { - const entries: LogEntry[] = [] - const logger = createNodeLogger({ - level: 'silent', - onLog: entry => entries.push(entry), - }) - - logger.info('message', { key: 'value' }) - - expect(entries[0].meta).toEqual({ key: 'value' }) - }) - - it('should allow changing log level at runtime', () => { - const logger = createNodeLogger({ level: 'info' }) - - expect(logger.getLevel()).toBe('info') - - logger.setLevel('debug') - expect(logger.getLevel()).toBe('debug') - }) -}) - -describe('createLogCollector', () => { - it('should collect log entries', () => { - const collector = createLogCollector() - - collector.add({ - level: 'info', - message: 'test', - timestamp: Date.now(), - }) - - expect(collector.entries).toHaveLength(1) - }) - - it('should respect maxEntries limit', () => { - const collector = createLogCollector({ maxEntries: 3 }) - - for (let i = 0; i < 5; i++) { - collector.add({ - level: 'info', - message: `message ${i}`, - timestamp: Date.now(), - }) - } - - expect(collector.entries).toHaveLength(3) - expect(collector.entries[0].message).toBe('message 2') - expect(collector.entries[2].message).toBe('message 4') - }) - - it('should filter entries by level', () => { - const collector = createLogCollector() - - collector.add({ level: 'debug', message: 'debug', timestamp: Date.now() }) - collector.add({ level: 'info', message: 'info', timestamp: Date.now() }) - collector.add({ level: 'warn', message: 'warn', timestamp: Date.now() }) - collector.add({ level: 'error', message: 'error', timestamp: Date.now() }) - - const warnings = collector.getEntries({ level: 'warn' }) - expect(warnings).toHaveLength(2) // warn and error - }) - - it('should filter entries by scope', () => { - const collector = createLogCollector() - - collector.add({ level: 'info', message: 'a', timestamp: Date.now(), scope: 'rpc' }) - collector.add({ level: 'info', message: 'b', timestamp: Date.now(), scope: 'rpc:call' }) - collector.add({ level: 'info', message: 'c', timestamp: Date.now(), scope: 'ws' }) - - const rpcLogs = collector.getEntries({ scope: 'rpc' }) - expect(rpcLogs).toHaveLength(2) - }) - - it('should notify subscribers on add', () => { - const collector = createLogCollector() - const callback = vi.fn() - - collector.subscribe(callback) - - // Called immediately with current entries - expect(callback).toHaveBeenCalledWith([]) - - collector.add({ level: 'info', message: 'test', timestamp: Date.now() }) - - expect(callback).toHaveBeenCalledTimes(2) - }) - - it('should allow unsubscribing', () => { - const collector = createLogCollector() - const callback = vi.fn() - - const unsubscribe = collector.subscribe(callback) - unsubscribe() - - collector.add({ level: 'info', message: 'test', timestamp: Date.now() }) - - // Only called once (initial call) - expect(callback).toHaveBeenCalledTimes(1) - }) - - it('should clear all entries', () => { - const collector = createLogCollector() - - collector.add({ level: 'info', message: 'test', timestamp: Date.now() }) - expect(collector.entries).toHaveLength(1) - - collector.clear() - expect(collector.entries).toHaveLength(0) - }) -}) diff --git a/packages/kit/src/utils/logger.ts b/packages/kit/src/utils/logger.ts index f942c037..ff77eb14 100644 --- a/packages/kit/src/utils/logger.ts +++ b/packages/kit/src/utils/logger.ts @@ -1,65 +1,42 @@ /** - * Unified Logger API + * Logger API using Consola * - * Entry point for the Vite DevTools logging system. - * Automatically selects the appropriate logger implementation based on environment. + * Provides a simple logger interface using unjs/consola, similar to Nuxt Kit. + * Exposed on context.logger for use in DevTools plugins. * * @example * ```ts - * import { createLogger } from '@vitejs/devtools-kit/utils/logger' + * export default defineDevToolsPlugin({ + * setup(context) { + * context.logger.info('Plugin initialized') + * context.logger.debug('Debug info', { config }) + * context.logger.warn('Deprecated option used') + * context.logger.error(new Error('Something went wrong')) * - * const logger = createLogger({ scope: 'my-plugin' }) - * logger.info('Plugin initialized') - * logger.debug('Debug info', { config }) - * logger.warn('Deprecated option used') - * logger.error(new Error('Something went wrong')) - * - * // Create child loggers for sub-components - * const rpcLogger = logger.child('rpc') - * rpcLogger.info('RPC connected') // [my-plugin:rpc] RPC connected + * // Create child loggers for sub-components + * const rpcLogger = context.logger.withTag('rpc') + * rpcLogger.info('RPC connected') // [rpc] RPC connected + * } + * }) * ``` */ -// Re-export collector -export { createLogCollector } from './log-collector' - -export type { LogCollectorOptions } from './log-collector' -export { logger as clientLogger, createClientLogger } from './logger-client' +import { consola } from 'consola' -// Environment-specific exports -// These are separate so bundlers can tree-shake the unused implementation - -export { createNodeLogger, logger as nodeLogger } from './logger-node' -// Re-export types -export type { - LogCollector, - LogEntry, - LogFilter, - Logger, - LoggerOptions, - LogLevel, -} from './logger-types' +export type { ConsolaInstance as Logger } from 'consola' /** - * Create a logger instance. - * - * In Node.js: Uses colored terminal output - * In Browser: Uses styled console output + * Create a logger instance with the given tag/scope. + * Similar to Nuxt Kit's useLogger. * - * @param options - Logger configuration options - * @returns Logger instance + * @param tag - Tag/scope for the logger (e.g., 'my-plugin', 'rpc') + * @returns Consola logger instance */ -export function createLogger(options?: import('./logger-types').LoggerOptions): import('./logger-types').Logger { - // Check for browser environment - if (typeof window !== 'undefined') { - // Dynamic import for tree-shaking in Node bundles - // eslint-disable-next-line ts/no-require-imports - const { createClientLogger } = require('./logger-client') - return createClientLogger(options) - } - else { - // eslint-disable-next-line ts/no-require-imports - const { createNodeLogger } = require('./logger-node') - return createNodeLogger(options) - } +export function createLogger(tag?: string): ReturnType { + return tag ? consola.withTag(tag) : consola } + +/** + * Default logger instance. + */ +export const logger = consola diff --git a/packages/kit/tsdown.config.ts b/packages/kit/tsdown.config.ts index d62da0d0..25e46a54 100644 --- a/packages/kit/tsdown.config.ts +++ b/packages/kit/tsdown.config.ts @@ -7,8 +7,6 @@ export default defineConfig({ 'utils/nanoid': 'src/utils/nanoid.ts', 'utils/shared-state': 'src/utils/shared-state.ts', 'utils/logger': 'src/utils/logger.ts', - 'utils/logger-node': 'src/utils/logger-node.ts', - 'utils/logger-client': 'src/utils/logger-client.ts', 'client': 'src/client/index.ts', }, exports: true, diff --git a/packages/vite/src/app/composables/dark.ts b/packages/vite/src/app/composables/dark.ts index b60743b7..22a380bc 100644 --- a/packages/vite/src/app/composables/dark.ts +++ b/packages/vite/src/app/composables/dark.ts @@ -1,3 +1,4 @@ +/* eslint-disable unimport/auto-insert */ import { useDark } from '@vueuse/core' export const isDark = useDark({ diff --git a/packages/vite/src/app/composables/module-graph.ts b/packages/vite/src/app/composables/module-graph.ts index 562ed817..971f8fc4 100644 --- a/packages/vite/src/app/composables/module-graph.ts +++ b/packages/vite/src/app/composables/module-graph.ts @@ -1,3 +1,4 @@ +/* eslint-disable unimport/auto-insert */ import type { ComputedRefWithControl } from '@vueuse/core' import type { HierarchyLink, HierarchyNode } from 'd3-hierarchy' import type { ComputedRef, InjectionKey, MaybeRef, Ref, ShallowReactive, ShallowRef } from 'vue' diff --git a/packages/vite/src/app/composables/rpc.ts b/packages/vite/src/app/composables/rpc.ts index 9514efe4..3842c4ea 100644 --- a/packages/vite/src/app/composables/rpc.ts +++ b/packages/vite/src/app/composables/rpc.ts @@ -1,3 +1,4 @@ +/* eslint-disable unimport/auto-insert */ import type {} from '@vitejs/devtools' import type { DevToolsRpcClient } from '@vitejs/devtools-kit/client' import type {} from '../../node/rpc' diff --git a/packages/vite/src/app/utils/color.ts b/packages/vite/src/app/utils/color.ts index cf54de75..c083989e 100644 --- a/packages/vite/src/app/utils/color.ts +++ b/packages/vite/src/app/utils/color.ts @@ -1,3 +1,4 @@ +/* eslint-disable unimport/auto-insert */ import { isDark } from '../composables/dark' export function getHashColorFromString( diff --git a/packages/vite/src/app/utils/filepath.ts b/packages/vite/src/app/utils/filepath.ts index 7a64cfca..1746de7a 100644 --- a/packages/vite/src/app/utils/filepath.ts +++ b/packages/vite/src/app/utils/filepath.ts @@ -1,3 +1,4 @@ +/* eslint-disable unimport/auto-insert */ import { relative } from 'pathe' import { getModuleNameFromPath, isPackageName } from '../../shared/utils/filepath' import { makeCachedFunction } from './cache' diff --git a/packages/vite/src/app/utils/icon.ts b/packages/vite/src/app/utils/icon.ts index e999b146..6db58dd3 100644 --- a/packages/vite/src/app/utils/icon.ts +++ b/packages/vite/src/app/utils/icon.ts @@ -1,3 +1,4 @@ +/* eslint-disable unimport/auto-insert */ import { makeCachedFunction } from './cache' export interface FilterMatchRule { diff --git a/packages/vite/src/shared/types/data.ts b/packages/vite/src/shared/types/data.ts index 745bade8..e2002024 100644 --- a/packages/vite/src/shared/types/data.ts +++ b/packages/vite/src/shared/types/data.ts @@ -1,3 +1,4 @@ +/* eslint-disable unimport/auto-insert */ import type { Asset as AssetInfo, ChunkImport, Chunk as ChunkInfo, HookResolveIdCallStart, ModuleImport, PluginItem, SessionMeta } from '@rolldown/debug' export type { ModuleImport } diff --git a/packages/vite/src/shared/types/vite.ts b/packages/vite/src/shared/types/vite.ts index aa48cab4..1e8da741 100644 --- a/packages/vite/src/shared/types/vite.ts +++ b/packages/vite/src/shared/types/vite.ts @@ -1,3 +1,4 @@ +/* eslint-disable unimport/auto-insert */ export interface ViteDevToolsPayload { timestamp: number hash: string diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 185316fe..e6f61a09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,9 @@ catalogs: cac: specifier: ^6.7.14 version: 6.7.14 + consola: + specifier: ^3.2.3 + version: 3.4.2 diff: specifier: ^8.0.2 version: 8.0.2 @@ -620,6 +623,9 @@ importers: birpc-x: specifier: catalog:deps version: 0.0.6 + consola: + specifier: catalog:deps + version: 3.4.2 immer: specifier: catalog:deps version: 11.1.3 @@ -1650,7 +1656,7 @@ packages: hasBin: true '@nuxt/vite-builder@https://pkg.pr.new/@nuxt/vite-builder@33682': - resolution: {tarball: https://pkg.pr.new/@nuxt/vite-builder@33682} + resolution: {integrity: sha512-z3NZ9bNKvbqC1Yj8tOfzzd4tCwe+QTeqoZwjYuzRmnLATbxt2ZULCtU6EmJO1WCNdE+dKbH9J6bJuAP07T8bdw==, tarball: https://pkg.pr.new/@nuxt/vite-builder@33682} version: 4.2.1 engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 253f600e..63460b50 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -33,6 +33,7 @@ catalogs: birpc: ^4.0.0 birpc-x: 0.0.6 cac: ^6.7.14 + consola: ^3.2.3 diff: ^8.0.2 get-port-please: ^3.2.0 h3: ^1.15.4 diff --git a/test/exports/@vitejs/devtools-kit.yaml b/test/exports/@vitejs/devtools-kit.yaml index 98f0383c..3d885d79 100644 --- a/test/exports/@vitejs/devtools-kit.yaml +++ b/test/exports/@vitejs/devtools-kit.yaml @@ -4,6 +4,9 @@ getDevToolsRpcClient: function ./utils/events: createEventEmitter: function +./utils/logger: + createLogger: function + logger: object ./utils/nanoid: nanoid: function ./utils/shared-state: diff --git a/tsconfig.base.json b/tsconfig.base.json index 7af0bd1e..9f44213e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,6 +27,9 @@ "@vitejs/devtools-kit/utils/events": [ "./packages/kit/src/utils/events.ts" ], + "@vitejs/devtools-kit/utils/logger": [ + "./packages/kit/src/utils/logger.ts" + ], "@vitejs/devtools-kit/utils/nanoid": [ "./packages/kit/src/utils/nanoid.ts" ], From 4c3f508a40fed3eb70126664f0124d56027f90d3 Mon Sep 17 00:00:00 2001 From: Adrian Darian Date: Tue, 13 Jan 2026 01:52:53 -0800 Subject: [PATCH 3/7] refactor(logging): remove legacy logging examples and RFC documents in favor of unified logging API --- .../0001-unified-logging-api-examples.txt | 273 --------- docs/rfcs/0001-unified-logging-api.txt | 557 ------------------ 2 files changed, 830 deletions(-) delete mode 100644 docs/rfcs/0001-unified-logging-api-examples.txt delete mode 100644 docs/rfcs/0001-unified-logging-api.txt diff --git a/docs/rfcs/0001-unified-logging-api-examples.txt b/docs/rfcs/0001-unified-logging-api-examples.txt deleted file mode 100644 index d2b4019b..00000000 --- a/docs/rfcs/0001-unified-logging-api-examples.txt +++ /dev/null @@ -1,273 +0,0 @@ -# Logger Migration Examples - -This document shows before/after examples of migrating existing `console.*` calls to the unified logger API. - -## Node.js (Server) Examples - -### packages/core/src/node/ws.ts - -**Before:** -```typescript -import c from 'ansis' -import { MARK_INFO } from './constants' - -// ... - -if (isClientAuthDisabled) { - console.warn('[Vite DevTools] Client authentication is disabled. Any browser can connect to the devtools and access to your server and filesystem.') -} - -// ... - -console.log(color`${MARK_INFO} Websocket client connected. [${meta.id}] [${meta.clientAuthId}] (${meta.isTrusted ? 'trusted' : 'untrusted'})`) - -// ... - -console.log(c.red`${MARK_INFO} Websocket client disconnected. [${meta.id}]`) - -// ... - -console.error(c.red`⬢ RPC error on executing "${c.bold(name)}":`) -console.error(error) -``` - -**After:** -```typescript -import { createNodeLogger } from '@vitejs/devtools-kit/utils/logger-node' - -const logger = createNodeLogger({ scope: 'vite-devtools:ws' }) - -// ... - -if (isClientAuthDisabled) { - logger.warn('Client authentication is disabled. Any browser can connect to the devtools and access to your server and filesystem.') -} - -// ... - -logger.info('Websocket client connected', { - id: meta.id, - clientAuthId: meta.clientAuthId, - trusted: meta.isTrusted -}) - -// ... - -logger.info('Websocket client disconnected', { id: meta.id }) - -// ... - -logger.error('RPC error on executing method', { method: name, error }) -``` - ---- - -### packages/core/src/node/context.ts - -**Before:** -```typescript -catch (error) { - console.error(`[Vite DevTools] Error setting up plugin ${plugin.name}:`, error) - throw error -} -``` - -**After:** -```typescript -import { createNodeLogger } from '@vitejs/devtools-kit/utils/logger-node' - -const logger = createNodeLogger({ scope: 'vite-devtools:context' }) - -// ... - -catch (error) { - logger.error(`Error setting up plugin ${plugin.name}`, { - plugin: plugin.name, - error: error as Error - }) - throw error -} -``` - ---- - -### packages/core/src/node/cli-commands.ts - -**Before:** -```typescript -console.log(c.green`${MARK_NODE} Vite DevTools started at`, c.green(`http://${host === '127.0.0.1' ? 'localhost' : host}:${port}`), '\n') - -console.log(c.cyan`${MARK_NODE} Building static Vite DevTools...`) -``` - -**After:** -```typescript -import { createNodeLogger } from '@vitejs/devtools-kit/utils/logger-node' - -const logger = createNodeLogger({ scope: 'vite-devtools:cli' }) - -// ... - -logger.info(`Vite DevTools started at http://${host === '127.0.0.1' ? 'localhost' : host}:${port}`) - -logger.info('Building static Vite DevTools...') -``` - ---- - -## Client (Browser) Examples - -### packages/core/src/client/inject/index.ts - -**Before:** -```typescript -console.log('[VITE DEVTOOLS] Client injected') - -// ... - -console.log('[VITE DEVTOOLS] Skipping in iframe') -``` - -**After:** -```typescript -import { createClientLogger } from '@vitejs/devtools-kit/utils/logger-client' - -const logger = createClientLogger({ scope: 'vite-devtools:inject' }) - -// ... - -logger.info('Client injected') - -// ... - -logger.info('Skipping in iframe') -``` - ---- - -### packages/core/src/client/webcomponents/state/setup-script.ts - -**Before:** -```typescript -.catch((error) => { - // TODO: maybe popup a error toast here? - // TODO: A unified logger API - console.error('[VITE DEVTOOLS] Error executing import action', error) - return Promise.reject(error) -}) -``` - -**After:** -```typescript -import { createClientLogger } from '@vitejs/devtools-kit/utils/logger-client' - -const logger = createClientLogger({ scope: 'vite-devtools:setup-script' }) - -// ... - - .catch((error) => { - logger.error('Error executing import action', { - entryId: id, - error: error as Error - }) - // TODO: integrate with toast notification system - return Promise.reject(error) - }) -``` - ---- - -### packages/vite/src/app/composables/rpc.ts - -**Before:** -```typescript -rpcOptions: { - onGeneralError: (e, name) => { - connectionState.error = e - console.error(`[vite-devtools] RPC error on executing "${name}":`) - }, - onFunctionError: (e, name) => { - connectionState.error = e - console.error(`[vite-devtools] RPC error on executing "${name}":`) - }, -}, -``` - -**After:** -```typescript -import { createClientLogger } from '@vitejs/devtools-kit/utils/logger-client' - -const logger = createClientLogger({ scope: 'vite-devtools:rpc' }) - -// ... - -rpcOptions: { - onGeneralError: (e, name) => { - connectionState.error = e - logger.error(`RPC error on executing "${name}"`, { method: name, error: e }) - }, - onFunctionError: (e, name) => { - connectionState.error = e - logger.error(`RPC error on executing "${name}"`, { method: name, error: e }) - }, -}, -``` - ---- - -## Log Aggregation Integration - -To enable the Logs panel in the DevTools, the context needs to collect logs: - -```typescript -// packages/core/src/node/context.ts -import { createNodeLogger, createLogCollector } from '@vitejs/devtools-kit/utils/logger' - -export async function createDevToolsContext(...) { - // Create log collector for the Logs panel - const logCollector = createLogCollector({ maxEntries: 2000 }) - - // Create logger that feeds into collector - const logger = createNodeLogger({ - scope: 'vite-devtools', - onLog: (entry) => logCollector.add(entry), - }) - - const context: DevToolsNodeContext = { - // ... existing properties - logger, - logs: logCollector, // Expose for RPC/UI - } - - // Add RPC method to fetch logs - context.rpc.register({ - name: 'vite:internal:logs:get', - type: 'action', - setup: () => async (filter) => { - return logCollector.getEntries(filter) - }, - }) - - // Add RPC method to subscribe to live logs (via shared state) - const logsSharedState = await context.rpc.sharedState.get('vite:internal:logs', { - initialValue: [], - }) - - logCollector.subscribe((entries) => { - // Only send last 100 entries for live view - logsSharedState.mutate(() => entries.slice(-100)) - }) -} -``` - -Then enable the Logs panel in `host-docks.ts`: - -```typescript -{ - type: '~builtin', - id: '~logs', - title: 'Logs', - icon: 'ph:notification-duotone', - isHidden: false, // Now enabled! -}, -``` diff --git a/docs/rfcs/0001-unified-logging-api.txt b/docs/rfcs/0001-unified-logging-api.txt deleted file mode 100644 index f4e049df..00000000 --- a/docs/rfcs/0001-unified-logging-api.txt +++ /dev/null @@ -1,557 +0,0 @@ -# RFC: Unified Logging API for Vite DevTools - -## Summary - -This RFC proposes a unified logging API for the Vite DevTools project to replace scattered `console.*` calls with a consistent, configurable, and feature-rich logging system that works across both Node.js (server) and browser (client) environments. - -## Motivation - -Currently, logging in Vite DevTools is inconsistent: - -1. **Inconsistent prefixes**: Some logs use `[VITE DEVTOOLS]`, others use `[Vite DevTools]`, `[vite-devtools]`, or `⬢` -2. **No log levels**: All logs go to console without filtering capability -3. **No structured logging**: Logs are plain strings with no metadata -4. **No centralized control**: Debug logs use `obug`/`createDebug`, but regular logs use raw `console.*` -5. **No log aggregation**: TODOs in the codebase mention wanting a "Logs" panel (see `host-docks.ts`) -6. **No error toast system**: TODOs mention wanting popup error toasts on the client - -### Current State - -```typescript -// Different prefixes used across the codebase: -console.log('[VITE DEVTOOLS] Client injected') -console.error('[Vite DevTools] Error setting up plugin...') -console.error('[vite-devtools] RPC error on executing...') -console.warn('[Vite DevTools] Client authentication is disabled...') -console.log(c.green`${MARK_NODE} Vite DevTools started at...`) -``` - -## Detailed Design - -### 1. Logger Interface - -```typescript -// packages/kit/src/utils/logger.ts - -export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent' - -export interface LogEntry { - level: LogLevel - message: string - timestamp: number - scope?: string - meta?: Record - error?: Error -} - -export interface LoggerOptions { - /** Minimum log level to output */ - level?: LogLevel - /** Scope/namespace for the logger (e.g., 'rpc', 'ws', 'client') */ - scope?: string - /** Whether to include timestamps */ - timestamps?: boolean - /** Custom log handler for aggregation */ - onLog?: (entry: LogEntry) => void -} - -export interface Logger { - debug: (message: string, meta?: Record) => void - info: (message: string, meta?: Record) => void - warn: (message: string, meta?: Record) => void - error: (message: string | Error, meta?: Record) => void - - /** Create a child logger with a sub-scope */ - child: (scope: string) => Logger - - /** Update logger options at runtime */ - setLevel: (level: LogLevel) => void -} -``` - -### 2. Node.js Logger Implementation - -```typescript -// packages/kit/src/utils/logger-node.ts - -import c from 'ansis' - -const LOG_LEVEL_PRIORITY: Record = { - debug: 0, - info: 1, - warn: 2, - error: 3, - silent: 4, -} - -const LEVEL_COLORS = { - debug: c.gray, - info: c.blue, - warn: c.yellow, - error: c.red, -} - -const LEVEL_ICONS = { - debug: '🔍', - info: 'ℹ', - warn: '⚠', - error: '✖', -} - -export function createNodeLogger(options: LoggerOptions = {}): Logger { - const { - level = 'info', - scope = 'vite-devtools', - timestamps = false, - onLog, - } = options - - let currentLevel = level - - function shouldLog(msgLevel: LogLevel): boolean { - return LOG_LEVEL_PRIORITY[msgLevel] >= LOG_LEVEL_PRIORITY[currentLevel] - } - - function formatScope(s: string): string { - return c.cyan`[${s}]` - } - - function log(entry: LogEntry): void { - onLog?.(entry) - - if (!shouldLog(entry.level) || entry.level === 'silent') - return - - const color = LEVEL_COLORS[entry.level] - const icon = LEVEL_ICONS[entry.level] - const parts: string[] = [] - - if (timestamps) { - parts.push(c.gray(new Date(entry.timestamp).toISOString())) - } - - parts.push(color(`${icon}`)) - - if (entry.scope) { - parts.push(formatScope(entry.scope)) - } - - parts.push(entry.message) - - const method = entry.level === 'debug' ? 'log' : entry.level - console[method](parts.join(' ')) - - if (entry.error) { - console.error(entry.error) - } - - if (entry.meta && Object.keys(entry.meta).length > 0) { - console.log(c.gray(' Meta:'), entry.meta) - } - } - - function createLogMethod(level: LogLevel) { - return (message: string | Error, meta?: Record) => { - const entry: LogEntry = { - level, - message: message instanceof Error ? message.message : message, - timestamp: Date.now(), - scope, - meta, - error: message instanceof Error ? message : undefined, - } - log(entry) - } - } - - return { - debug: createLogMethod('debug'), - info: createLogMethod('info'), - warn: createLogMethod('warn'), - error: createLogMethod('error'), - - child(childScope: string): Logger { - return createNodeLogger({ - ...options, - level: currentLevel, - scope: scope ? `${scope}:${childScope}` : childScope, - onLog, - }) - }, - - setLevel(newLevel: LogLevel) { - currentLevel = newLevel - }, - } -} - -// Singleton for convenience -export const logger = createNodeLogger() -``` - -### 3. Client Logger Implementation - -```typescript -// packages/kit/src/utils/logger-client.ts - -import type { LogEntry, Logger, LoggerOptions, LogLevel } from './logger' - -const LOG_LEVEL_PRIORITY: Record = { - debug: 0, - info: 1, - warn: 2, - error: 3, - silent: 4, -} - -const LEVEL_STYLES = { - debug: 'color: gray', - info: 'color: #3b82f6', - warn: 'color: #f59e0b', - error: 'color: #ef4444; font-weight: bold', -} - -export function createClientLogger(options: LoggerOptions = {}): Logger { - const { - level = 'info', - scope = 'vite-devtools', - timestamps = false, - onLog, - } = options - - let currentLevel = level - - function shouldLog(msgLevel: LogLevel): boolean { - return LOG_LEVEL_PRIORITY[msgLevel] >= LOG_LEVEL_PRIORITY[currentLevel] - } - - function log(entry: LogEntry): void { - onLog?.(entry) - - if (!shouldLog(entry.level) || entry.level === 'silent') - return - - const style = LEVEL_STYLES[entry.level] - const prefix = entry.scope ? `[${entry.scope}]` : '[vite-devtools]' - const time = timestamps ? `${new Date(entry.timestamp).toISOString()} ` : '' - - const method = entry.level === 'debug' ? 'log' : entry.level - console[method]( - `%c${time}${prefix}%c ${entry.message}`, - style, - 'color: inherit', - ...(entry.meta ? [entry.meta] : []), - ) - - if (entry.error) { - console.error(entry.error) - } - } - - function createLogMethod(level: LogLevel) { - return (message: string | Error, meta?: Record) => { - const entry: LogEntry = { - level, - message: message instanceof Error ? message.message : message, - timestamp: Date.now(), - scope, - meta, - error: message instanceof Error ? message : undefined, - } - log(entry) - } - } - - return { - debug: createLogMethod('debug'), - info: createLogMethod('info'), - warn: createLogMethod('warn'), - error: createLogMethod('error'), - - child(childScope: string): Logger { - return createClientLogger({ - ...options, - level: currentLevel, - scope: scope ? `${scope}:${childScope}` : childScope, - onLog, - }) - }, - - setLevel(newLevel: LogLevel) { - currentLevel = newLevel - }, - } -} - -// Singleton for convenience -export const logger = createClientLogger() -``` - -### 4. Universal Entry Point - -```typescript -// packages/kit/src/utils/logger.ts - -export type * from './logger-types' - -// Re-export the appropriate logger based on environment -// This allows tree-shaking and proper bundling - -export function createLogger(options?: LoggerOptions): Logger { - if (typeof window === 'undefined') { - // Node.js environment - const { createNodeLogger } = await import('./logger-node') - return createNodeLogger(options) - } - else { - // Browser environment - const { createClientLogger } = await import('./logger-client') - return createClientLogger(options) - } -} -``` - -### 5. Log Aggregation for DevTools Panel - -To support the "Logs" panel mentioned in the TODO, we can collect logs: - -```typescript -// packages/kit/src/utils/log-collector.ts - -import type { LogEntry } from './logger-types' - -export interface LogCollector { - entries: LogEntry[] - maxEntries: number - - add: (entry: LogEntry) => void - clear: () => void - getEntries: (filter?: { level?: LogLevel, scope?: string }) => LogEntry[] - - // For reactive updates - subscribe: (callback: (entries: LogEntry[]) => void) => () => void -} - -export function createLogCollector(maxEntries = 1000): LogCollector { - const entries: LogEntry[] = [] - const subscribers = new Set<(entries: LogEntry[]) => void>() - - function notify() { - subscribers.forEach(cb => cb([...entries])) - } - - return { - entries, - maxEntries, - - add(entry: LogEntry) { - entries.push(entry) - if (entries.length > maxEntries) { - entries.shift() - } - notify() - }, - - clear() { - entries.length = 0 - notify() - }, - - getEntries(filter) { - return entries.filter((entry) => { - if (filter?.level && entry.level !== filter.level) - return false - if (filter?.scope && !entry.scope?.includes(filter.scope)) - return false - return true - }) - }, - - subscribe(callback) { - subscribers.add(callback) - return () => subscribers.delete(callback) - }, - } -} -``` - -### 6. Integration with DevTools Context - -```typescript -// In packages/core/src/node/context.ts - -import { createNodeLogger, createLogCollector } from '@vitejs/devtools-kit/utils/logger' - -export async function createDevToolsContext(...) { - const logCollector = createLogCollector() - - const logger = createNodeLogger({ - scope: 'vite-devtools', - onLog: (entry) => logCollector.add(entry), - }) - - const context: DevToolsNodeContext = { - // ... existing properties - logger, - logCollector, // Expose for the Logs panel - } - - // Usage in setup - logger.info('DevTools context created', { cwd, mode: viteConfig.command }) - - // For plugins - for (const plugin of plugins) { - const pluginLogger = logger.child(`plugin:${plugin.name}`) - try { - await plugin.devtools?.setup?.(context, pluginLogger) - } catch (error) { - pluginLogger.error(error as Error) - throw error - } - } -} -``` - -### 7. Error Toast System (Client) - -```typescript -// packages/kit/src/client/toast.ts - -import type { LogEntry } from '../utils/logger-types' - -export interface ToastOptions { - duration?: number - type?: 'info' | 'warn' | 'error' | 'success' -} - -export interface ToastManager { - show: (message: string, options?: ToastOptions) => void - showFromLog: (entry: LogEntry) => void - dismiss: (id: string) => void - dismissAll: () => void -} - -// Implementation would integrate with the UI framework (Vue) -// This is just the interface definition -``` - -## Migration Guide - -### Before - -```typescript -// packages/core/src/node/ws.ts -console.warn('[Vite DevTools] Client authentication is disabled...') -console.log(color`${MARK_INFO} Websocket client connected...`) -console.error(c.red`⬢ RPC error on executing...`) -``` - -### After - -```typescript -// packages/core/src/node/ws.ts -import { logger } from '@vitejs/devtools-kit/utils/logger' - -const wsLogger = logger.child('ws') - -wsLogger.warn('Client authentication is disabled. Any browser can connect to the devtools.') -wsLogger.info('Websocket client connected', { id: meta.id, trusted: meta.isTrusted }) -wsLogger.error('RPC error on executing', { method: name, error }) -``` - -## Exports - -The logger should be exported from `@vitejs/devtools-kit`: - -```typescript -// packages/kit/src/index.ts -export { createLogCollector, createLogger } from './utils/logger' -export type { LogCollector, LogEntry, Logger, LoggerOptions, LogLevel } from './utils/logger-types' -``` - -And available via subpath: -```typescript -// For tree-shaking when only logger is needed -import { createLogger } from '@vitejs/devtools-kit/utils/logger' -``` - -## Configuration - -### Environment Variables - -```bash -# Set global log level -VITE_DEVTOOLS_LOG_LEVEL=debug - -# Enable specific scopes (similar to DEBUG env var pattern) -VITE_DEVTOOLS_LOG_SCOPES=vite-devtools:ws,vite-devtools:rpc -``` - -### Runtime Configuration - -```typescript -// In vite.config.ts -export default defineConfig({ - plugins: [ - devtools({ - logger: { - level: 'debug', - timestamps: true, - } - }) - ] -}) -``` - -## Implementation Plan - -### Phase 1: Core Logger (packages/kit) -1. Create logger types in `packages/kit/src/utils/logger-types.ts` -2. Implement Node logger in `packages/kit/src/utils/logger-node.ts` -3. Implement Client logger in `packages/kit/src/utils/logger-client.ts` -4. Create log collector for aggregation -5. Export from kit package - -### Phase 2: Integration (packages/core) -1. Add logger to `DevToolsNodeContext` -2. Migrate `packages/core/src/node/*.ts` console calls -3. Add `onLog` handler for log aggregation - -### Phase 3: Client Integration -1. Migrate `packages/core/src/client/**/*.ts` console calls -2. Implement toast notifications for errors -3. Create Logs panel UI component - -### Phase 4: Cleanup -1. Remove `// TODO: A unified logger API` comments -2. Enable the Logs dock panel (`isHidden: false`) -3. Update documentation - -## Alternatives Considered - -### 1. Use existing library (pino, winston, consola) -- **Pros**: Battle-tested, feature-rich -- **Cons**: Bundle size, over-engineered for devtools needs, may not work well in both Node and browser - -### 2. Extend `obug`/`createDebug` -- **Pros**: Already used in the project -- **Cons**: Debug-only, no log levels, not designed for production logging - -### 3. Keep console.* with standardized prefixes -- **Pros**: Simple, no new dependencies -- **Cons**: No log levels, no aggregation, no structured data - -## Open Questions - -1. Should logs be persisted to disk in dev mode? -2. Should we integrate with Vite's own logger? -3. What should the max buffer size be for log aggregation? -4. Should we support custom log formatters? - -## References - -- [Vite Logger](https://vite.dev/guide/api-javascript.html#custom-logger) -- [consola](https://github.com/unjs/consola) - Similar unified logging approach -- Related TODOs in codebase: - - `packages/core/src/client/webcomponents/state/setup-script.ts:20` - - `packages/core/src/node/host-docks.ts:34` From 1cccaab077dacc8d633d5508f30a4e3e0a473534 Mon Sep 17 00:00:00 2001 From: Adrian Darian Date: Tue, 13 Jan 2026 02:03:35 -0800 Subject: [PATCH 4/7] chore(eslint): disable 'unimport/auto-insert' rule for specific files --- eslint.config.js | 15 +++++++++++++++ packages/vite/src/app/composables/dark.ts | 1 - packages/vite/src/app/composables/module-graph.ts | 1 - packages/vite/src/app/composables/rpc.ts | 1 - packages/vite/src/app/utils/color.ts | 1 - packages/vite/src/app/utils/filepath.ts | 1 - packages/vite/src/app/utils/icon.ts | 1 - packages/vite/src/shared/types/data.ts | 1 - packages/vite/src/shared/types/vite.ts | 1 - 9 files changed, 15 insertions(+), 8 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index b0051131..864b08c8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -12,6 +12,21 @@ export default antfu({ 'no-console': 'off', }, }) + .append({ + files: [ + './packages/vite/src/app/composables/dark.ts', + './packages/vite/src/app/composables/module-graph.ts', + './packages/vite/src/app/composables/rpc.ts', + './packages/vite/src/app/utils/color.ts', + './packages/vite/src/app/utils/filepath.ts', + './packages/vite/src/app/utils/icon.ts', + './packages/vite/src/shared/types/data.ts', + './packages/vite/src/shared/types/vite.ts', + ], + rules: { + 'unimport/auto-insert': 'off', + }, + }) .removeRules( 'vue/no-template-shadow', 'pnpm/json-prefer-workspace-settings', diff --git a/packages/vite/src/app/composables/dark.ts b/packages/vite/src/app/composables/dark.ts index 22a380bc..b60743b7 100644 --- a/packages/vite/src/app/composables/dark.ts +++ b/packages/vite/src/app/composables/dark.ts @@ -1,4 +1,3 @@ -/* eslint-disable unimport/auto-insert */ import { useDark } from '@vueuse/core' export const isDark = useDark({ diff --git a/packages/vite/src/app/composables/module-graph.ts b/packages/vite/src/app/composables/module-graph.ts index 971f8fc4..562ed817 100644 --- a/packages/vite/src/app/composables/module-graph.ts +++ b/packages/vite/src/app/composables/module-graph.ts @@ -1,4 +1,3 @@ -/* eslint-disable unimport/auto-insert */ import type { ComputedRefWithControl } from '@vueuse/core' import type { HierarchyLink, HierarchyNode } from 'd3-hierarchy' import type { ComputedRef, InjectionKey, MaybeRef, Ref, ShallowReactive, ShallowRef } from 'vue' diff --git a/packages/vite/src/app/composables/rpc.ts b/packages/vite/src/app/composables/rpc.ts index 3842c4ea..9514efe4 100644 --- a/packages/vite/src/app/composables/rpc.ts +++ b/packages/vite/src/app/composables/rpc.ts @@ -1,4 +1,3 @@ -/* eslint-disable unimport/auto-insert */ import type {} from '@vitejs/devtools' import type { DevToolsRpcClient } from '@vitejs/devtools-kit/client' import type {} from '../../node/rpc' diff --git a/packages/vite/src/app/utils/color.ts b/packages/vite/src/app/utils/color.ts index c083989e..cf54de75 100644 --- a/packages/vite/src/app/utils/color.ts +++ b/packages/vite/src/app/utils/color.ts @@ -1,4 +1,3 @@ -/* eslint-disable unimport/auto-insert */ import { isDark } from '../composables/dark' export function getHashColorFromString( diff --git a/packages/vite/src/app/utils/filepath.ts b/packages/vite/src/app/utils/filepath.ts index 1746de7a..7a64cfca 100644 --- a/packages/vite/src/app/utils/filepath.ts +++ b/packages/vite/src/app/utils/filepath.ts @@ -1,4 +1,3 @@ -/* eslint-disable unimport/auto-insert */ import { relative } from 'pathe' import { getModuleNameFromPath, isPackageName } from '../../shared/utils/filepath' import { makeCachedFunction } from './cache' diff --git a/packages/vite/src/app/utils/icon.ts b/packages/vite/src/app/utils/icon.ts index 6db58dd3..e999b146 100644 --- a/packages/vite/src/app/utils/icon.ts +++ b/packages/vite/src/app/utils/icon.ts @@ -1,4 +1,3 @@ -/* eslint-disable unimport/auto-insert */ import { makeCachedFunction } from './cache' export interface FilterMatchRule { diff --git a/packages/vite/src/shared/types/data.ts b/packages/vite/src/shared/types/data.ts index e2002024..745bade8 100644 --- a/packages/vite/src/shared/types/data.ts +++ b/packages/vite/src/shared/types/data.ts @@ -1,4 +1,3 @@ -/* eslint-disable unimport/auto-insert */ import type { Asset as AssetInfo, ChunkImport, Chunk as ChunkInfo, HookResolveIdCallStart, ModuleImport, PluginItem, SessionMeta } from '@rolldown/debug' export type { ModuleImport } diff --git a/packages/vite/src/shared/types/vite.ts b/packages/vite/src/shared/types/vite.ts index 1e8da741..aa48cab4 100644 --- a/packages/vite/src/shared/types/vite.ts +++ b/packages/vite/src/shared/types/vite.ts @@ -1,4 +1,3 @@ -/* eslint-disable unimport/auto-insert */ export interface ViteDevToolsPayload { timestamp: number hash: string From 97ffa0b2ca623276b3327113bb0068e8a6dae2f4 Mon Sep 17 00:00:00 2001 From: Adrian Darian Date: Tue, 13 Jan 2026 02:22:38 -0800 Subject: [PATCH 5/7] chore(eslint): update file patterns for TypeScript linting --- eslint.config.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 864b08c8..a3830962 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,16 +13,7 @@ export default antfu({ }, }) .append({ - files: [ - './packages/vite/src/app/composables/dark.ts', - './packages/vite/src/app/composables/module-graph.ts', - './packages/vite/src/app/composables/rpc.ts', - './packages/vite/src/app/utils/color.ts', - './packages/vite/src/app/utils/filepath.ts', - './packages/vite/src/app/utils/icon.ts', - './packages/vite/src/shared/types/data.ts', - './packages/vite/src/shared/types/vite.ts', - ], + files: ['./packages/vite/src/app/**/*.ts', './packages/vite/src/shared/**/*.ts'], rules: { 'unimport/auto-insert': 'off', }, From 2517cd1dfbe0770ec8ded65864dd1770cd94ac75 Mon Sep 17 00:00:00 2001 From: Adrian Darian Date: Tue, 13 Jan 2026 22:02:07 -0800 Subject: [PATCH 6/7] feat(turbo): add new task configurations for dev preparation and build dependencies --- turbo.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/turbo.json b/turbo.json index af907315..2ac9deda 100644 --- a/turbo.json +++ b/turbo.json @@ -1,10 +1,29 @@ { "$schema": "https://turbo.build/schema.json", "tasks": { + "dev:prepare": { + "outputLogs": "new-only", + "outputs": [ + "**/.nuxt/**" + ] + }, "build": { "outputLogs": "new-only", "outputs": [ "dist/**" + ], + "dependsOn": [ + "^build" + ] + }, + "@vitejs/devtools-vite#build": { + "dependsOn": [ + "@vitejs/devtools-vite#dev:prepare" + ] + }, + "@vitejs/devtools#build": { + "dependsOn": [ + "@vitejs/devtools-vite#dev:prepare" ] } } From f5fc99f6aff01ac553bdcf3ef16067f436f4c338 Mon Sep 17 00:00:00 2001 From: Adrian Darian Date: Tue, 13 Jan 2026 22:10:09 -0800 Subject: [PATCH 7/7] feat(App.vue): pass context prop to DockEntriesWithCategories component --- packages/core/src/client/standalone/App.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/client/standalone/App.vue b/packages/core/src/client/standalone/App.vue index 19766337..1cc39e05 100644 --- a/packages/core/src/client/standalone/App.vue +++ b/packages/core/src/client/standalone/App.vue @@ -53,6 +53,7 @@ function switchEntry(id: string) {