UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

363 lines (361 loc) • 10.5 kB
let node_fs = require("node:fs"); let node_path = require("node:path"); let node_fs_promises = require("node:fs/promises"); //#region src/services/dev/logs.ts const ESC = "\x1B"; const BEL = "\x07"; const ANSI_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]|${ESC}\\][^${BEL}]*${BEL}`, "g"); const stripAnsi = (input) => input.replace(ANSI_RE, ""); const LEVEL_RANK = { error: 0, warn: 1, info: 2, debug: 3 }; const LOG_LEVELS = [ "error", "warn", "info", "debug" ]; function isDebug(value) { return value === "true" || value === "1"; } function resolveLogLevel(env = process.env, flag) { if (flag && LOG_LEVELS.includes(flag)) return flag; if (env.BOS_LOG_LEVEL && LOG_LEVELS.includes(env.BOS_LOG_LEVEL)) return env.BOS_LOG_LEVEL; if (isDebug(env.DEBUG)) return "debug"; return "warn"; } function toEffectLogLevel(level) { switch (level) { case "error": return "Error"; case "warn": return "Warn"; case "info": return "Info"; case "debug": return "Debug"; } } const STACK_FRAME_RE = /^\s+at\s/; const countBraces = (line) => { let count = 0; for (const ch of line) if (ch === "{") count += 1; else if (ch === "}") count -= 1; return count; }; const makeNormalizerState = () => ({ pending: [], openBraces: 0 }); const pushLine = (state, line) => { state.pending.push(line); state.openBraces = Math.max(0, state.openBraces + countBraces(line)); }; const resetState = (state) => { state.pending = []; state.openBraces = 0; }; const feedLine = (state, line, meta) => { if (state.pending.length === 0) { state.meta = meta; pushLine(state, line); return null; } if (state.openBraces > 0 || STACK_FRAME_RE.test(line) || /^\s*\{/.test(line)) { const closeOnBalance = state.openBraces > 0; pushLine(state, line); if (closeOnBalance && state.openBraces === 0) { const completed = { text: state.pending.join("\n"), meta: state.meta }; resetState(state); return completed; } return null; } const completed = { text: state.pending.join("\n"), meta: state.meta }; resetState(state); state.meta = meta; pushLine(state, line); return completed; }; const drainState = (state) => { if (state.pending.length === 0) return null; const completed = { text: state.pending.join("\n"), meta: state.meta }; resetState(state); return completed; }; const WARN_RE = /(?:^|\s)WARN\b|\[WARN\]|(?:^|\s)\w*Warning:/i; const DEBUG_RE = /(?:^|\s)DEBUG\b|\[DEBUG\]/i; const BUILD_DIAGNOSTIC_RE = /^(?:File: |\s*[×✖]\s)/; const EXIT_LINE_RE = /^Process exited (?:after|before) ready \((?:exit code: (\d+)|signal: ([A-Z]+))\)/; const CLEAN_SIGNALS = /* @__PURE__ */ new Set(["SIGTERM", "SIGINT"]); const CATEGORY_PATTERNS = [ { category: "shutdown", patterns: [ EXIT_LINE_RE, /\[SIGTERM\]/i, /shutting down/i, /graceful shutdown/i ] }, { category: "mf", patterns: [ /\[ ?Federation Runtime ?\]/, /\[MF\]/, /Module Federation/i, /Executing an Effect versioned/, /you may want to dedupe the effect dependencies/, /\[IntegrityMonitor\]/, /\[Plugins\]/ ] }, { category: "db", patterns: [ /\[Database\]/i, /\bpostgres\b/i, /\bdrizzle\b/i, /\bpglite\b/i ] }, { category: "banner", patterns: [ /🚀|📡|📖|💚/, /➜/, /^[┌┐└┘│─┬┴├┤╔╗╚╝║═]/ ] }, { category: "build", patterns: [ /rspack\.config\.js not found/, /rspack|rsbuild/i, /compiled (?:success|with|in)/i, /built in \d+/i, /webpack/i, /\bHMR\b/, /building for production/i, BUILD_DIAGNOSTIC_RE ] }, { category: "lifecycle", patterns: [ /\bready in\b/i, /listening (?:on|at)/i, /server (?:is )?running/i ] } ]; const classifyCategory = (text) => { for (const { category, patterns } of CATEGORY_PATTERNS) if (patterns.some((pattern) => pattern.test(text))) return category; return "other"; }; function classifyEvent(raw) { const text = stripAnsi(raw.line); const exitMatch = text.match(EXIT_LINE_RE); const category = classifyCategory(text); let level; let isError = raw.isError === true; if (exitMatch) { if (exitMatch[2]) level = CLEAN_SIGNALS.has(exitMatch[2]) ? "info" : "error"; else level = exitMatch[1] === "0" ? "info" : "error"; isError = level === "error"; } else if (BUILD_DIAGNOSTIC_RE.test(text)) { level = "error"; isError = true; } else if (WARN_RE.test(text)) { level = "warn"; isError = false; } else if (DEBUG_RE.test(text)) { level = "debug"; isError = false; } else if (isError) level = "error"; else level = "info"; return { timestamp: raw.timestamp ?? Date.now(), source: raw.source, line: text, isError, level, category }; } function passesLevel(event, level) { return LEVEL_RANK[event.level] <= LEVEL_RANK[level]; } var DbCollapser = class { runs = /* @__PURE__ */ new Set(); feed(event) { if (event.category === "db" && event.level !== "error") { if (!this.runs.has(event.source)) { this.runs.add(event.source); return { ...event, line: "db ready", isError: false, level: "info", collapsed: true }; } return null; } this.runs.delete(event.source); return { ...event, collapsed: false }; } }; function createLogPipeline(options) { const level = options.level ?? resolveLogLevel(); const sinks = options.sinks; const normalizers = /* @__PURE__ */ new Map(); const collapser = new DbCollapser(); const stateFor = (source) => { let state = normalizers.get(source); if (!state) { state = makeNormalizerState(); normalizers.set(source, state); } return state; }; const emit = (meta, text) => { const event = classifyEvent({ source: meta.source, line: text, isError: meta.isError, timestamp: meta.timestamp }); sinks.file(event); if (!passesLevel(event, level)) return; const collapsed = collapser.feed(event); if (!collapsed) return; sinks.export(collapsed); sinks.display(collapsed); }; return { ingest: (raw) => { const clean = stripAnsi(raw.line); if (!clean.trim()) return; const state = stateFor(raw.source); const completed = feedLine(state, clean, { source: raw.source, isError: raw.isError, timestamp: raw.timestamp }); if (completed !== null) emit(completed.meta, completed.text); }, flushSource: (source) => { const state = normalizers.get(source); if (!state) return; const completed = drainState(state); if (completed !== null) emit(completed.meta, completed.text); }, flush: () => { for (const state of normalizers.values()) { const completed = drainState(state); if (completed !== null) emit(completed.meta, completed.text); } } }; } function getBosDir(configDir) { return (0, node_path.join)(configDir, ".bos"); } function getLogsDir(configDir) { return (0, node_path.join)(getBosDir(configDir), "logs"); } function formatLogLine(entry) { const ts = new Date(entry.timestamp).toISOString(); const prefix = entry.isError ? "ERR" : "OUT"; const clean = stripAnsi(entry.line); const head = `[${ts}] [${entry.source}] [${prefix}] `; const pad = " ".repeat(head.length); const [first, ...rest] = clean.split("\n"); return [`${head}${first}`, ...rest.map((line) => `${pad}${line}`)].join("\n"); } async function createDevLogger(configDir, description) { const dir = getLogsDir(configDir); if (!(0, node_fs.existsSync)(dir)) await (0, node_fs_promises.mkdir)(dir, { recursive: true }); const now = /* @__PURE__ */ new Date(); const ts = now.toISOString().replace(/[:.]/g, "-").slice(0, 19); const logFile = (0, node_path.join)(dir, `dev-${ts}-${process.pid}.log`); const latestFile = (0, node_path.join)(dir, `dev-latest-${process.pid}.log`); const header = `# everything-dev dev session: ${description}\n# Started: ${now.toISOString()}\n\n`; await (0, node_fs_promises.writeFile)(logFile, header, "utf8"); await (0, node_fs_promises.writeFile)(latestFile, header, "utf8"); let chain = Promise.resolve(); const enqueue = (fn) => { chain = chain.then(fn, fn); return chain; }; return { logFile, latestFile, write: (entry) => enqueue(async () => { const line = `${formatLogLine(entry)}\n`; await (0, node_fs_promises.appendFile)(logFile, line); await (0, node_fs_promises.appendFile)(latestFile, line); }), readLatest: async (opts) => { const text = await (0, node_fs_promises.readFile)(latestFile, "utf8").catch(() => ""); const tail = opts?.tail; if (!tail || tail <= 0) return text; const lines = text.split("\n"); return lines.slice(Math.max(0, lines.length - tail)).join("\n"); } }; } function resolveDevLatestFile(configDir) { const dir = getLogsDir(configDir); const latestFile = (0, node_path.join)(dir, "dev-latest.log"); if ((0, node_fs.existsSync)(latestFile)) return latestFile; const STARTED_RE = /^# Started: (.+)$/m; const startedMsOf = (name) => { const header = (0, node_fs.readFileSync)((0, node_path.join)(dir, name), "utf8").slice(0, 256); const started = STARTED_RE.exec(header)?.[1]; const parsed = started ? Date.parse(started) : NaN; return Number.isNaN(parsed) ? (0, node_fs.statSync)((0, node_path.join)(dir, name)).mtimeMs : parsed; }; const pidOf = (name) => Number(/^dev-latest-(\d+)\.log$/.exec(name)?.[1] ?? 0); const candidates = (0, node_fs.readdirSync)(dir).filter((name) => /^dev-latest-\d+\.log$/.test(name)).map((name) => ({ name, startedMs: startedMsOf(name), pid: pidOf(name) })).sort((a, b) => b.startedMs - a.startedMs || b.pid - a.pid || a.name.localeCompare(b.name)); return candidates.length > 0 ? (0, node_path.join)(dir, candidates[0].name) : latestFile; } async function readDevLatestLog(configDir, opts) { const latestFile = resolveDevLatestFile(configDir); const text = await (0, node_fs_promises.readFile)(latestFile, "utf8").catch(() => ""); const tail = opts?.tail; if (!tail || tail <= 0) return text; const lines = text.split("\n"); return lines.slice(Math.max(0, lines.length - tail)).join("\n"); } //#endregion exports.classifyEvent = classifyEvent; exports.createDevLogger = createDevLogger; exports.createLogPipeline = createLogPipeline; exports.formatLogLine = formatLogLine; exports.getBosDir = getBosDir; exports.getLogsDir = getLogsDir; exports.isDebug = isDebug; exports.passesLevel = passesLevel; exports.readDevLatestLog = readDevLatestLog; exports.resolveDevLatestFile = resolveDevLatestFile; exports.resolveLogLevel = resolveLogLevel; exports.stripAnsi = stripAnsi; exports.toEffectLogLevel = toEffectLogLevel; //# sourceMappingURL=logs.cjs.map