everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
363 lines (361 loc) • 10.5 kB
JavaScript
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