@nitra/consola
Version:
consola with filename
327 lines (278 loc) • 11.6 kB
JavaScript
/* 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
}