UNPKG

@nitra/consola

Version:
327 lines (278 loc) 11.6 kB
/* global location, process, import */ import { consola } from 'consola' import { OpenTelemetryReporter } from './otel-reporter.js' const options = {} // Vite Debug mode (injected at build time) if (globalThis['__CONSOLA_LEVEL_DEBUG__'] !== undefined) { options.level = 4 } else if (typeof location !== 'undefined' && location.protocol === 'http:') { options.level = 4 } // HTML Popup Reporter — дублювання повідомлень у вигляді спливаючих блоків (без canvas) const POPUP_PADDING = 12 const POPUP_LABEL_PADDING = 8 const POPUP_LINE_HEIGHT = 20 const POPUP_BUTTON_SIZE = 18 const POPUP_BUTTON_PADDING = 6 const POPUP_MIN_HEIGHT = 36 const POPUP_MAX_MESSAGE_LENGTH = 500 const POPUP_STYLES = ` #consola-popup-container { position: fixed; inset: 0; pointer-events: none; z-index: 999999; } .consola-popup { pointer-events: auto; position: fixed; left: 20px; max-width: calc(100vw - 40px); min-height: ${POPUP_MIN_HEIGHT}px; display: flex; align-items: stretch; background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border: 1px solid #e0e0e0; font: 13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .consola-popup-label { flex-shrink: 0; padding: 0 ${POPUP_LABEL_PADDING}px; display: flex; align-items: center; justify-content: center; color: #fff; font: 11px monospace; font-weight: bold; text-transform: lowercase; } .consola-popup-content { flex: 1; min-width: 0; padding: ${POPUP_PADDING}px; display: flex; flex-wrap: wrap; align-items: flex-start; gap: 4px 8px; } .consola-popup-text { flex: 1 1 auto; min-width: 0; color: #212121; line-height: ${POPUP_LINE_HEIGHT}px; white-space: pre-wrap; word-break: break-word; } .consola-popup-filelink { flex: 0 0 auto; color: #1976D2; font: 11px monospace; text-decoration: underline; cursor: pointer; } .consola-popup-filelink:hover { text-decoration: underline; } .consola-popup-close { flex-shrink: 0; width: ${POPUP_BUTTON_SIZE}px; height: ${POPUP_BUTTON_SIZE}px; margin: ${POPUP_BUTTON_PADDING}px; padding: 0; border: none; background: transparent; color: #999; font-size: 16px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 2px; } .consola-popup-close:hover { background: rgba(0,0,0,0.06); color: #666; } ` class HtmlPopupReporter { constructor() { this.container = null this.messages = [] this.messageId = 0 this.init() } init() { if (typeof document === 'undefined') return const doInit = () => { if (!document.body) { setTimeout(doInit, 10) return } let container = document.querySelector('#consola-popup-container') if (!container) { const style = document.createElement('style') style.textContent = POPUP_STYLES document.head.append(style) container = document.createElement('div') container.id = 'consola-popup-container' document.body.append(container) } this.container = container this.animate() } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', doInit) } else { doInit() } } getColorForType(type) { const colors = { trace: '#9E9E9E', debug: '#607D8B', info: '#2196F3', log: '#2196F3', warn: '#FFC107', error: '#D32F2F', fatal: '#7B1FA2', success: '#4CAF50', start: '#00BCD4', box: '#9E9E9E', ready: '#4CAF50', fail: '#D32F2F' } return colors[type] || colors.log || '#616161' } getTypeLabel(type) { return type || 'log' } getFileInfo() { try { const stack = new Error('Stack trace').stack if (!stack) return null const skipPatterns = ['HtmlPopupReporter', 'CanvasPopupReporter', 'browser.js', 'consola', 'createLogger'] const lines = stack.split('\n').slice(4, 15) for (const line of lines) { const trimmed = line.trim() const shouldSkip = skipPatterns.some(pattern => trimmed.includes(pattern)) if (!shouldSkip) { const match = trimmed.match(/\(?([^()]+):(\d+):(\d+)\)?/) || trimmed.match(/at\s+[^(]*\(?([^:]+):(\d+):(\d+)\)?/) if (match) { let file = match[1].trim() const lineNum = match[2] // Очищаємо ім'я файлу if (file.includes('://')) { file = file.slice(file.indexOf('://') + 3).slice(file.indexOf('/')) } if (file.includes('/src/')) { file = file.slice(file.indexOf('/src/') + 5) } const queryIndex = file.indexOf('?') if (queryIndex > 0) file = file.slice(0, queryIndex) const hashIndex = file.indexOf('#') if (hashIndex > 0) file = file.slice(0, hashIndex) if (file.startsWith('/')) file = file.slice(1) if (file && lineNum) { return { file, line: parseInt(lineNum, 10), column: parseInt(match[3], 10), fullPath: match[0] } } } } } } catch { // Ігноруємо помилки } return null } formatMessage(logObj) { const args = logObj.args || [] if (args.length === 0) return '' const message = args .map(arg => { if (arg instanceof Error) { let msg = `${arg.name || 'Error'}: ${arg.message || ''}` if (arg.stack && arg.stack !== arg.message) { msg += '\n' + arg.stack.split('\n').slice(0, 3).join('\n') } return msg } if (arg === null) return 'null' if (arg === undefined) return 'undefined' if (typeof arg === 'object') { try { const str = arg.toString?.() if (str && str !== '[object Object]') return str return JSON.stringify(arg, null, 2) } catch { return String(arg) } } return String(arg) }) .join(' ') return message.length > POPUP_MAX_MESSAGE_LENGTH ? message.slice(0, POPUP_MAX_MESSAGE_LENGTH) + '...' : message } log(logObj) { if (!this.container) return const text = this.formatMessage(logObj) if (!text) return const type = logObj.type || 'log' const color = this.getColorForType(type) const typeLabel = this.getTypeLabel(type) const id = this.messageId++ const fileInfo = this.getFileInfo() const popup = document.createElement('div') popup.className = 'consola-popup' popup.dataset.id = String(id) popup.style.bottom = `${window.innerHeight}px` popup.style.backgroundColor = '#fff' const label = document.createElement('span') label.className = 'consola-popup-label' label.style.backgroundColor = color label.textContent = typeLabel const content = document.createElement('div') content.className = 'consola-popup-content' const textEl = document.createElement('div') textEl.className = 'consola-popup-text' textEl.textContent = text content.append(textEl) if (fileInfo) { const fileLink = document.createElement('span') fileLink.className = 'consola-popup-filelink' fileLink.textContent = `${fileInfo.file}:${fileInfo.line}` fileLink.role = 'button' fileLink.tabIndex = 0 fileLink.addEventListener('click', e => { e.preventDefault() console.log(`%c${fileInfo.file}:${fileInfo.line}`, 'color: #1976D2; text-decoration: underline;') }) content.append(fileLink) } const closeBtn = document.createElement('button') closeBtn.type = 'button' closeBtn.className = 'consola-popup-close' closeBtn.setAttribute('aria-label', 'Close') closeBtn.textContent = '×' closeBtn.addEventListener('click', () => { const idx = this.messages.findIndex(m => m.id === id) if (idx !== -1) { const entry = this.messages[idx] if (entry.el && entry.el.parentNode) entry.el.remove() this.messages.splice(idx, 1) } }) popup.append(label, content, closeBtn) this.container.append(popup) const height = popup.offsetHeight this.messages.push({ id, el: popup, targetBottom: 20, currentBottom: window.innerHeight, height }) } animate() { if (!this.container) return let offset = 20 for (let i = this.messages.length - 1; i >= 0; i--) { const msg = this.messages[i] if (msg.el && msg.el.parentNode) { const h = msg.el.offsetHeight msg.height = h msg.targetBottom = offset msg.currentBottom += (msg.targetBottom - msg.currentBottom) * 0.1 msg.el.style.bottom = `${Math.round(msg.currentBottom)}px` offset += h + 10 } } requestAnimationFrame(() => this.animate()) } } // Перевіряємо змінну середовища VITE_CONSOLA_POPUP_DEBUG let isPopupDebugEnabled = false // Vite замінює import.meta.env.VITE_* під час збірки // Використовуємо прямий доступ до import.meta.env, оскільки Vite обробляє це під час збірки if (typeof document !== 'undefined') { try { // Vite замінює цей код під час збірки // eslint-disable-next-line no-undef const envValue = import.meta.env.VITE_CONSOLA_POPUP_DEBUG if (envValue !== undefined && envValue !== null && envValue !== '') { // Vite завжди повертає рядки для змінних середовища isPopupDebugEnabled = String(envValue).toLowerCase() === 'true' || envValue === true } } catch { // Ігноруємо помилки, якщо import.meta недоступний } } // Створюємо один екземпляр репортера, якщо потрібно // НЕ встановлюємо options.reporters, щоб стандартний BrowserReporter працював // Додаємо наш репортер після створення екземпляра через addReporter let htmlPopupReporter = null if (isPopupDebugEnabled && typeof document !== 'undefined') { htmlPopupReporter = new HtmlPopupReporter() } const defaultConsola = consola.create(options) // Додаємо HTML репортер до default екземпляра, якщо потрібно (не замінює стандартний) if (isPopupDebugEnabled && typeof document !== 'undefined' && htmlPopupReporter) { defaultConsola.addReporter(htmlPopupReporter) } // Перевіряємо змінні середовища Vite для OpenTelemetry експорту let openTelemetryReporter = null if (import.meta.env.VITE_OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) { openTelemetryReporter = new OpenTelemetryReporter() defaultConsola.addReporter(openTelemetryReporter) } export default defaultConsola // Експортуємо consola з нашими опціями, а не базовий export { defaultConsola as consola } // Експортуємо OpenTelemetryReporter та createOpenTelemetryReporter для прямого використання export { createOpenTelemetryReporter, OpenTelemetryReporter } from './otel-reporter.js' /** * pass import.meta.url * example: const consola = createLogger(import.meta.url) * * @param {String} _url - The import.meta.url or file URL to use for logger creation * @returns {Consola} A consola logger instance */ export const createLogger = _url => { return defaultConsola }