UNPKG

@steve02081504/virtual-console

Version:

A virtual console for capturing and manipulating terminal output.

283 lines (265 loc) 9.69 kB
import { renderAnsi, renderHtml as renderHtmlFromSegments, renderPlain as renderPlainFromSegments } from '../format/render.mjs' import { buildArgsSegments } from '../format/segments.mjs' import { DEFAULT_SNAPSHOT_DEPTH, createExpansionScope, resolvePrimaryCallsiteFromSegments, serializeArgSnapshot, } from './snapshot.mjs' import { getStackInfo } from './stack.mjs' /** * 将 console 方法名转换为语义级别。 * @param {string} methodName - console 方法名。 * @returns {string} 语义级别。 */ export function methodNameToLevel(methodName) { return { dir: 'log', freshLine: 'log', trace: 'debug', stdout: 'log', stderr: 'error', }[methodName] ?? methodName } /** * 单条日志条目:`segments` 由 {@link LogEntry#toSegments} 按需构造;`stdout`/`stderr` 带 `text`。 */ export class LogEntry { /** * @param {object} options - 日志条目选项。 * @param {string} options.method - 日志方法名。 * @param {any[]} options.args - 日志参数;流为 `[text]`。 * @param {ReturnType<typeof getStackInfo>} options.stack - 调用栈。 * @param {number} options.timestamp - 日志时间戳(默认 Date.now())。 * @param {boolean} options.supportsAnsi - 是否支持 ANSI 序列。 */ constructor({ method, args = [], stack = [], timestamp = Date.now(), supportsAnsi = false }) { this.level = methodNameToLevel(method) this.method = method this.stack = stack this.timestamp = timestamp this.supportsAnsi = supportsAnsi this.args = args } /** * 展示来源:优先片段中首个 Error 的栈帧,否则为捕获调用栈中第一条。 * @returns {import('../shared.d.mts').StackFrame | null} 无路径帧时为 null。 */ get primaryCallsite() { return resolvePrimaryCallsiteFromSegments(this.toSegments(), this.stack) } /** * 终端 ANSI 串(`stdout`/`stderr` 为原始流文本)。 * @returns {string} `renderAnsi(toSegments())` 或流文本。 */ toString() { return renderAnsi(this.toSegments(), { colorize: this.supportsAnsi }) } /** * 剥除转义与样式后的纯文本。 * @returns {string} `renderPlain(toSegments())`。 */ toPlainText() { return renderPlainFromSegments(this.toSegments()) } /** * 与 `toSegments` 同管线下的 HTML。 * @returns {string} `renderHtml(toSegments(), …)`。 */ toHtml() { return renderHtmlFromSegments(this.toSegments(), { supportsAnsi: this.supportsAnsi }) } /** * 将捕获参数序列化为可 JSON 的快照树数组。 * @param {number} [maxDepth=DEFAULT_SNAPSHOT_DEPTH] - 各参数的递归深度上限。 * @returns {import('../shared.d.mts').ArgSnapshot[]} 与参数个数相同的快照数组。 */ serializeArgs(maxDepth = DEFAULT_SNAPSHOT_DEPTH) { return this.args.map(arg => serializeArgSnapshot(arg, { maxDepth })) } /** * 结构化片段:`log`/`dir`/`trace` 等在末尾含 `{ kind: 'text', text: '\\n' }`;流不含。 * @returns {import('../shared.d.mts').LogSegment[]} 可供 `renderPlain` / `renderAnsi` / `renderHtml` 消费。 */ toSegments() { const expansionScope = createExpansionScope(this) const maxDepth = DEFAULT_SNAPSHOT_DEPTH return [...buildArgsSegments(this.args, expansionScope, maxDepth), { kind: 'text', text: '\n' }] } /** * JSON 传输视图(默认与 wire 载荷字段对齐,供子类重载扩展)。 * @returns {Record<string, unknown>} JSON 友好对象。 */ toJSON() { return { method: this.method, timestamp: this.timestamp, segments: this.toSegments(), stack: this.stack, } } } /** * `stdout` / `stderr` 流日志条目:原样透传文本,不追加换行片段。 */ class StreamLogEntry extends LogEntry { /** * @param {object} options - 日志条目选项。 * @param {'stdout' | 'stderr'} options.method - 流方法名。 * @param {any[]} options.args - 原始流参数。 * @param {ReturnType<typeof getStackInfo>} options.stack - 调用栈。 * @param {number} options.timestamp - 日志时间戳(默认 Date.now())。 * @param {boolean} options.supportsAnsi - 是否支持 ANSI 序列。 */ constructor({ method, args = [], stack = [], timestamp = Date.now(), supportsAnsi = false }) { const text = String(args?.[0] ?? '') super({ method, args: [text], stack, timestamp, supportsAnsi }) this.text = text } /** @returns {string} 原始流文本(不追加换行)。 */ toString() { return this.text } /** * @returns {import('../shared.d.mts').LogSegment[]} 流文本片段;空文本返回空数组。 */ toSegments() { return [{ kind: 'text', text: this.text }] } } /** * 将 `console.dir` 第二参数收敛为可 JSON 传输的浅层选项(仅 `depth`、`colors`)。 * @param {unknown} raw - 原始 options。 * @returns {import('../shared.d.mts').DirOptionsPayload | undefined} 无有效字段时为 `undefined`。 */ function normalizeDirOptionsPayload(raw) { return { depth: raw?.depth ?? DEFAULT_SNAPSHOT_DEPTH, colors: raw?.colors ?? true, } } /** `console.dir` 条目:携带单个 value 段及可选 dirOptions。 */ class DirLogEntry extends LogEntry { /** * @param {object} options - 日志条目选项。 * @param {'dir'} options.method - 方法名。 * @param {any[]} options.args - 原始参数。 * @param {ReturnType<typeof getStackInfo>} options.stack - 调用栈。 * @param {number} options.timestamp - 日志时间戳(默认 Date.now())。 * @param {boolean} options.supportsAnsi - 是否支持 ANSI 序列。 */ constructor({ method, args = [], stack = [], timestamp = Date.now(), supportsAnsi = false }) { super({ method, args, stack, timestamp, supportsAnsi }) } /** * @returns {import('../shared.d.mts').LogSegment[]} value + 末尾换行片段。 */ toSegments() { const expansionScope = createExpansionScope(this) const maxDepth = DEFAULT_SNAPSHOT_DEPTH const [subject, dirOptions] = this.args return [{ kind: 'value', snapshot: serializeArgSnapshot(subject, { maxDepth, expansionScope }), dirOptions: normalizeDirOptionsPayload(dirOptions), }, { kind: 'text', text: '\n' }] } } /** `console.trace` 条目:普通参数片段 + trace 快照片段。 */ class TraceLogEntry extends LogEntry { /** * @param {object} options - 日志条目选项。 * @param {'trace'} options.method - 方法名。 * @param {any[]} options.args - 原始参数。 * @param {ReturnType<typeof getStackInfo>} options.stack - 调用栈。 * @param {number} options.timestamp - 日志时间戳(默认 Date.now())。 * @param {boolean} options.supportsAnsi - 是否支持 ANSI 序列。 */ constructor({ method, args = [], stack = [], timestamp = Date.now(), supportsAnsi = false }) { super({ method, args, stack, timestamp, supportsAnsi }) } /** * @returns {import('../shared.d.mts').LogSegment[]} 参数片段、trace 片段与末尾换行。 */ toSegments() { const expansionScope = createExpansionScope(this) const maxDepth = DEFAULT_SNAPSHOT_DEPTH const segs = [ ...buildArgsSegments(this.args, expansionScope, maxDepth), { kind: 'trace', stack: this.stack }, ] return [...segs, { kind: 'text', text: '\n' }] } } /** * `console.freshLine` 条目:首个参数为行 id,不进入日志格式化。 */ export class FreshLineLogEntry extends LogEntry { /** * @param {object} options - 日志条目选项。 * @param {'freshLine'} options.method - 方法名。 * @param {any[]} options.args - 原始参数,首项应为 id。 * @param {ReturnType<typeof getStackInfo>} options.stack - 调用栈。 * @param {number} options.timestamp - 日志时间戳(默认 Date.now())。 * @param {boolean} options.supportsAnsi - 是否支持 ANSI 序列。 */ constructor({ method, args = [], stack = [], timestamp = Date.now(), supportsAnsi = false }) { super({ method, args, stack, timestamp, supportsAnsi }) this.id = String(args[0] ?? '') } /** * 参数快照跳过第一个 id 参数,避免污染正文格式化。 * @param {number} [maxDepth=DEFAULT_SNAPSHOT_DEPTH] - 各参数的递归深度上限。 * @returns {import('../shared.d.mts').ArgSnapshot[]} 除 id 外参数的快照数组。 */ serializeArgs(maxDepth = DEFAULT_SNAPSHOT_DEPTH) { return this.args.slice(1).map(arg => serializeArgSnapshot(arg, { maxDepth })) } /** * 结构化片段跳过首个 id 参数,和普通 `log` 保持同渲染管线。 * @returns {import('../shared.d.mts').LogSegment[]} 可供渲染器消费的片段。 */ toSegments() { const expansionScope = createExpansionScope(this) const maxDepth = DEFAULT_SNAPSHOT_DEPTH return [...buildArgsSegments(this.args.slice(1), expansionScope, maxDepth), { kind: 'text', text: '\n' }] } /** * freshLine 的 JSON 传输视图:在基类字段上追加 id。 * @returns {Record<string, unknown>} JSON 友好对象。 */ toJSON() { return { ...super.toJSON(), id: this.id, } } } const methodToConstructorMap = { stdout: StreamLogEntry, stderr: StreamLogEntry, dir: DirLogEntry, trace: TraceLogEntry, freshLine: FreshLineLogEntry, } /** * 由方法名选择对应的日志条目构造器。 * @param {string} method - 日志方法名(如 `log` / `dir` / `trace` / `stdout`)。 * @returns {typeof LogEntry} 对应的构造器;未知方法回退为 {@link LogEntry}。 */ function methodToConstructor(method) { return methodToConstructorMap[method] ?? LogEntry } /** * @param {object} options - 见 {@link LogEntry} 构造函数。 * @returns {LogEntry} 新分配的日志条目实例。 */ export function newLogEntry(options) { return new (methodToConstructor(options.method))(options) }