{"version":3,"file":"dev-log-pipeline.mjs","names":[],"sources":["../src/dev-log-pipeline.ts"],"sourcesContent":["const ESC = \"\\x1b\";\nconst BEL = \"\\x07\";\nconst ANSI_RE = new RegExp(`${ESC}\\\\[[0-?]*[ -/]*[@-~]|${ESC}\\\\][^${BEL}]*${BEL}`, \"g\");\n\nexport const stripAnsi = (input: string): string => input.replace(ANSI_RE, \"\");\n\nexport type LogLevel = \"error\" | \"warn\" | \"info\" | \"debug\";\n\nexport type LogCategory = \"banner\" | \"build\" | \"db\" | \"mf\" | \"lifecycle\" | \"shutdown\" | \"other\";\n\nexport interface RawLogLine {\n  source: string;\n  line: string;\n  isError?: boolean;\n  timestamp?: number;\n}\n\nexport interface LogEvent {\n  timestamp: number;\n  source: string;\n  line: string;\n  isError: boolean;\n  level: LogLevel;\n  category: LogCategory;\n}\n\nconst LEVEL_RANK: Record<LogLevel, number> = { error: 0, warn: 1, info: 2, debug: 3 };\n\nconst LOG_LEVELS: readonly LogLevel[] = [\"error\", \"warn\", \"info\", \"debug\"];\n\nexport interface LogLevelEnv {\n  BOS_LOG_LEVEL?: string;\n  DEBUG?: string;\n}\n\nexport function isDebug(value: string | undefined): boolean {\n  return value === \"true\" || value === \"1\";\n}\n\nexport function resolveLogLevel(\n  env: LogLevelEnv = process.env as LogLevelEnv,\n  flag?: string,\n): LogLevel {\n  if (flag && (LOG_LEVELS as readonly string[]).includes(flag)) return flag as LogLevel;\n  if (env.BOS_LOG_LEVEL && (LOG_LEVELS as readonly string[]).includes(env.BOS_LOG_LEVEL)) {\n    return env.BOS_LOG_LEVEL as LogLevel;\n  }\n  if (isDebug(env.DEBUG)) return \"debug\";\n  return \"warn\";\n}\n\nexport type EffectLogLevel = \"Error\" | \"Warn\" | \"Info\" | \"Debug\";\n\nexport function toEffectLogLevel(level: LogLevel): EffectLogLevel {\n  switch (level) {\n    case \"error\":\n      return \"Error\";\n    case \"warn\":\n      return \"Warn\";\n    case \"info\":\n      return \"Info\";\n    case \"debug\":\n      return \"Debug\";\n  }\n}\n\nconst STACK_FRAME_RE = /^\\s+at\\s/;\n\nconst countBraces = (line: string): number => {\n  let count = 0;\n  for (const ch of line) {\n    if (ch === \"{\") count += 1;\n    else if (ch === \"}\") count -= 1;\n  }\n  return count;\n};\n\ninterface PendingMeta {\n  source: string;\n  isError?: boolean;\n  timestamp?: number;\n}\n\ninterface NormalizerState {\n  pending: string[];\n  openBraces: number;\n  meta?: PendingMeta;\n}\n\nconst makeNormalizerState = (): NormalizerState => ({ pending: [], openBraces: 0 });\n\nconst pushLine = (state: NormalizerState, line: string) => {\n  state.pending.push(line);\n  state.openBraces = Math.max(0, state.openBraces + countBraces(line));\n};\n\nconst resetState = (state: NormalizerState) => {\n  state.pending = [];\n  state.openBraces = 0;\n};\n\ninterface CompletedEvent {\n  text: string;\n  meta: PendingMeta;\n}\n\nconst feedLine = (\n  state: NormalizerState,\n  line: string,\n  meta: PendingMeta,\n): CompletedEvent | null => {\n  if (state.pending.length === 0) {\n    state.meta = meta;\n    pushLine(state, line);\n    return null;\n  }\n\n  const isContinuation = state.openBraces > 0 || STACK_FRAME_RE.test(line) || /^\\s*\\{/.test(line);\n\n  if (isContinuation) {\n    const closeOnBalance = state.openBraces > 0;\n    pushLine(state, line);\n    if (closeOnBalance && state.openBraces === 0) {\n      const completed = { text: state.pending.join(\"\\n\"), meta: state.meta! };\n      resetState(state);\n      return completed;\n    }\n    return null;\n  }\n\n  const completed = { text: state.pending.join(\"\\n\"), meta: state.meta! };\n  resetState(state);\n  state.meta = meta;\n  pushLine(state, line);\n  return completed;\n};\n\nconst drainState = (state: NormalizerState): CompletedEvent | null => {\n  if (state.pending.length === 0) return null;\n  const completed = { text: state.pending.join(\"\\n\"), meta: state.meta! };\n  resetState(state);\n  return completed;\n};\n\nexport function normalizeLines(lines: string[]): string[] {\n  const state = makeNormalizerState();\n  const events: string[] = [];\n  for (const line of lines) {\n    const completed = feedLine(state, line, { source: \"\" });\n    if (completed !== null) events.push(completed.text);\n  }\n  const last = drainState(state);\n  if (last !== null) events.push(last.text);\n  return events;\n}\n\nconst WARN_RE = /(?:^|\\s)WARN\\b|\\[WARN\\]|(?:^|\\s)\\w*Warning:/i;\nconst DEBUG_RE = /(?:^|\\s)DEBUG\\b|\\[DEBUG\\]/i;\n\n// rspack/rsbuild write the \"Build error:\" title to stderr with an empty\n// message and the actual diagnostics (`File: …`, `× …`) to stdout. Promote\n// those diagnostic lines so they pass the default warn display filter.\nconst BUILD_DIAGNOSTIC_RE = /^(?:File: |\\s*[×✖]\\s)/;\n\nconst EXIT_LINE_RE =\n  /^Process exited (?:after|before) ready \\((?:exit code: (\\d+)|signal: ([A-Z]+))\\)/;\n\nconst CLEAN_SIGNALS = new Set([\"SIGTERM\", \"SIGINT\"]);\n\nconst CATEGORY_PATTERNS: Array<{ category: LogCategory; patterns: RegExp[] }> = [\n  {\n    category: \"shutdown\",\n    patterns: [EXIT_LINE_RE, /\\[SIGTERM\\]/i, /shutting down/i, /graceful shutdown/i],\n  },\n  {\n    category: \"mf\",\n    patterns: [\n      /\\[ ?Federation Runtime ?\\]/,\n      /\\[MF\\]/,\n      /Module Federation/i,\n      /Executing an Effect versioned/,\n      /you may want to dedupe the effect dependencies/,\n      /\\[IntegrityMonitor\\]/,\n      /\\[Plugins\\]/,\n    ],\n  },\n  {\n    category: \"db\",\n    patterns: [/\\[Database\\]/i, /\\bpostgres\\b/i, /\\bdrizzle\\b/i, /\\bpglite\\b/i],\n  },\n  {\n    category: \"banner\",\n    patterns: [/🚀|📡|📖|💚/, /➜/, /^[┌┐└┘│─┬┴├┤╔╗╚╝║═]/],\n  },\n  {\n    category: \"build\",\n    patterns: [\n      /rspack\\.config\\.js not found/,\n      /rspack|rsbuild/i,\n      /compiled (?:success|with|in)/i,\n      /built in \\d+/i,\n      /webpack/i,\n      /\\bHMR\\b/,\n      /building for production/i,\n      BUILD_DIAGNOSTIC_RE,\n    ],\n  },\n  {\n    category: \"lifecycle\",\n    patterns: [/\\bready in\\b/i, /listening (?:on|at)/i, /server (?:is )?running/i],\n  },\n];\n\nconst classifyCategory = (text: string): LogCategory => {\n  for (const { category, patterns } of CATEGORY_PATTERNS) {\n    if (patterns.some((pattern) => pattern.test(text))) return category;\n  }\n  return \"other\";\n};\n\nexport function classifyEvent(raw: RawLogLine): LogEvent {\n  const text = stripAnsi(raw.line);\n  const exitMatch = text.match(EXIT_LINE_RE);\n  const category = classifyCategory(text);\n\n  let level: LogLevel;\n  let isError = raw.isError === true;\n\n  if (exitMatch) {\n    if (exitMatch[2]) {\n      level = CLEAN_SIGNALS.has(exitMatch[2]) ? \"info\" : \"error\";\n    } else {\n      level = exitMatch[1] === \"0\" ? \"info\" : \"error\";\n    }\n    isError = level === \"error\";\n  } else if (BUILD_DIAGNOSTIC_RE.test(text)) {\n    level = \"error\";\n    isError = true;\n  } else if (WARN_RE.test(text)) {\n    level = \"warn\";\n    isError = false;\n  } else if (DEBUG_RE.test(text)) {\n    level = \"debug\";\n    isError = false;\n  } else if (isError) {\n    level = \"error\";\n  } else {\n    level = \"info\";\n  }\n\n  return {\n    timestamp: raw.timestamp ?? Date.now(),\n    source: raw.source,\n    line: text,\n    isError,\n    level,\n    category,\n  };\n}\n\nexport function passesLevel(event: LogEvent, level: LogLevel): boolean {\n  return LEVEL_RANK[event.level] <= LEVEL_RANK[level];\n}\n\nexport interface CollapsedDbEvent extends LogEvent {\n  collapsed: boolean;\n}\n\nclass DbCollapser {\n  private runs = new Set<string>();\n\n  feed(event: LogEvent): CollapsedDbEvent | null {\n    if (event.category === \"db\" && event.level !== \"error\") {\n      if (!this.runs.has(event.source)) {\n        this.runs.add(event.source);\n        return { ...event, line: \"db ready\", isError: false, level: \"info\", collapsed: true };\n      }\n      return null;\n    }\n    this.runs.delete(event.source);\n    return { ...event, collapsed: false };\n  }\n}\n\nexport interface LogPipelineSinks {\n  display: (event: LogEvent) => void;\n  file: (event: LogEvent) => void;\n  export: (event: LogEvent) => void;\n}\n\nexport interface LogPipeline {\n  ingest: (raw: RawLogLine) => void;\n  flush: () => void;\n}\n\nexport function createLogPipeline(options: {\n  level?: LogLevel;\n  sinks: LogPipelineSinks;\n}): LogPipeline {\n  const level = options.level ?? resolveLogLevel();\n  const sinks = options.sinks;\n  const normalizers = new Map<string, NormalizerState>();\n  const collapser = new DbCollapser();\n\n  const stateFor = (source: string): NormalizerState => {\n    let state = normalizers.get(source);\n    if (!state) {\n      state = makeNormalizerState();\n      normalizers.set(source, state);\n    }\n    return state;\n  };\n\n  const emit = (meta: PendingMeta, text: string) => {\n    const event = classifyEvent({\n      source: meta.source,\n      line: text,\n      isError: meta.isError,\n      timestamp: meta.timestamp,\n    });\n    sinks.file(event);\n    if (!passesLevel(event, level)) return;\n    const collapsed = collapser.feed(event);\n    if (!collapsed) return;\n    sinks.export(collapsed);\n    sinks.display(collapsed);\n  };\n\n  return {\n    ingest: (raw) => {\n      const clean = stripAnsi(raw.line);\n      if (!clean.trim()) return;\n      const state = stateFor(raw.source);\n      const completed = feedLine(state, clean, {\n        source: raw.source,\n        isError: raw.isError,\n        timestamp: raw.timestamp,\n      });\n      if (completed !== null) emit(completed.meta, completed.text);\n    },\n    flush: () => {\n      for (const state of normalizers.values()) {\n        const completed = drainState(state);\n        if (completed !== null) emit(completed.meta, completed.text);\n      }\n    },\n  };\n}\n"],"mappings":"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"}