@steve02081504/virtual-console 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser.mjs +192 -0
- package/main.mjs +9 -197
- package/node.mjs +197 -0
- package/package.json +1 -1
package/browser.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { FullProxy } from 'full-proxy'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 存储原始的浏览器 console 对象。
|
|
5
|
+
*/
|
|
6
|
+
const originalConsole = window.console
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 格式化 console 参数为字符串。
|
|
10
|
+
* @param {any[]} args - console 方法接收的参数数组。
|
|
11
|
+
* @returns {string} 格式化后的单行字符串。
|
|
12
|
+
*/
|
|
13
|
+
function formatArgs(args) {
|
|
14
|
+
return args.map(arg => {
|
|
15
|
+
if (Object(arg) instanceof String) return arg
|
|
16
|
+
if (arg instanceof Error && arg.stack) return arg.stack
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(arg, null, '\t')
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return String(arg)
|
|
22
|
+
}
|
|
23
|
+
}).join(' ')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 创建一个虚拟控制台,用于捕获输出,同时可以选择性地将输出传递给真实的浏览器控制台。
|
|
28
|
+
*/
|
|
29
|
+
export class VirtualConsole {
|
|
30
|
+
/** @type {string} - 捕获的所有输出 */
|
|
31
|
+
outputs = ''
|
|
32
|
+
|
|
33
|
+
/** @type {object} - 最终合并后的配置项 */
|
|
34
|
+
options
|
|
35
|
+
|
|
36
|
+
/** @type {Console} - 用于 realConsoleOutput 的底层控制台实例 */
|
|
37
|
+
#base_console
|
|
38
|
+
|
|
39
|
+
/** @private @type {string | null} - 用于 freshLine 功能,记录上一次 freshLine 的 ID */
|
|
40
|
+
#loggedFreshLineId = null
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {object} [options={}] - 配置选项。
|
|
44
|
+
* @param {boolean} [options.realConsoleOutput=false] - 如果为 true,则在捕获输出的同时,也调用底层控制台进行实际输出。
|
|
45
|
+
* @param {boolean} [options.recordOutput=true] - 如果为 true,则捕获输出并保存在 outputs 属性中。
|
|
46
|
+
* @param {Console} [options.base_console=window.console] - 用于 realConsoleOutput 的底层控制台实例。
|
|
47
|
+
*/
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
this.#base_console = options.base_console || originalConsole
|
|
50
|
+
delete options.base_console
|
|
51
|
+
|
|
52
|
+
this.options = {
|
|
53
|
+
realConsoleOutput: false,
|
|
54
|
+
recordOutput: true,
|
|
55
|
+
...options,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const methods = ['log', 'info', 'warn', 'debug', 'error', 'table', 'dir', 'assert', 'count', 'countReset', 'time', 'timeLog', 'timeEnd', 'group', 'groupCollapsed', 'groupEnd']
|
|
59
|
+
for (const method of methods)
|
|
60
|
+
if (typeof this.#base_console[method] === 'function')
|
|
61
|
+
this[method] = (...args) => {
|
|
62
|
+
this.#loggedFreshLineId = null // 任何常规输出都会中断 freshLine 序列
|
|
63
|
+
|
|
64
|
+
if (this.options.recordOutput)
|
|
65
|
+
this.outputs += formatArgs(args) + '\n'
|
|
66
|
+
|
|
67
|
+
// 实际输出
|
|
68
|
+
if (this.options.realConsoleOutput)
|
|
69
|
+
this.#base_console[method](...args)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
this.freshLine = this.freshLine.bind(this)
|
|
75
|
+
this.clear = this.clear.bind(this)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 在新的异步上下文中执行fn,并将该上下文的控制台替换为此对象。
|
|
80
|
+
* 这是对 Node.js 中 AsyncLocalStorage.run 的浏览器模拟。
|
|
81
|
+
* @template T
|
|
82
|
+
* @overload
|
|
83
|
+
* @param {() => T | Promise<T>} fn - 在新的异步上下文中执行的函数。
|
|
84
|
+
* @returns {Promise<T>} 返回 fn 函数的 Promise 结果。
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* 将当前“异步上下文”中的控制台替换为此对象。
|
|
88
|
+
* [浏览器限制] 这在浏览器中是全局性的,会影响所有后续代码,直到被再次更改。
|
|
89
|
+
* @overload
|
|
90
|
+
* @returns {void}
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* 若提供fn,则在新的异步上下文中执行fn,并将fn上下文的控制台替换为此对象。
|
|
94
|
+
* 否则,将当前异步上下文中的控制台替换为此对象。
|
|
95
|
+
* @param {(() => T | Promise<T>) | undefined} [fn]
|
|
96
|
+
* @returns {Promise<T> | void}
|
|
97
|
+
*/
|
|
98
|
+
hookAsyncContext(fn) {
|
|
99
|
+
if (fn) return consoleReflectRun(this, fn)
|
|
100
|
+
else consoleReflectSet(this)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 在终端中打印一行。
|
|
106
|
+
* [浏览器限制] 由于浏览器控制台不支持 ANSI 光标移动,
|
|
107
|
+
* 此方法无法像在 Node.js 终端中那样覆盖上一行。
|
|
108
|
+
* 它目前等同于 console.log。
|
|
109
|
+
* @param {string} id - 用于标识行的唯一ID (在浏览器中未使用)。
|
|
110
|
+
* @param {...any} args - 要打印的内容。
|
|
111
|
+
*/
|
|
112
|
+
freshLine(id, ...args) {
|
|
113
|
+
// 在浏览器中,我们无法移动光标,所以这基本上就是一个 log
|
|
114
|
+
// 我们仍然可以模拟逻辑,以防未来浏览器支持类似功能
|
|
115
|
+
// 注意:我们不像原生版本那样清除上一行,因为做不到
|
|
116
|
+
this.log(...args)
|
|
117
|
+
this.#loggedFreshLineId = id
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 清空捕获的输出,并可以选择性地清空真实控制台。
|
|
122
|
+
*/
|
|
123
|
+
clear() {
|
|
124
|
+
this.#loggedFreshLineId = null
|
|
125
|
+
this.outputs = ''
|
|
126
|
+
if (this.options.realConsoleOutput)
|
|
127
|
+
this.#base_console.clear()
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const defaultConsole = new VirtualConsole({
|
|
133
|
+
base_console: originalConsole,
|
|
134
|
+
recordOutput: false,
|
|
135
|
+
realConsoleOutput: true,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
export const globalConsoleAdditionalProperties = {}
|
|
139
|
+
|
|
140
|
+
// 模拟 AsyncLocalStorage 的上下文存储
|
|
141
|
+
let currentAsyncConsole = null
|
|
142
|
+
|
|
143
|
+
/** @type {() => VirtualConsole} */
|
|
144
|
+
let consoleReflect = () => currentAsyncConsole ?? defaultConsole
|
|
145
|
+
|
|
146
|
+
/** @type {(value: VirtualConsole) => void} */
|
|
147
|
+
let consoleReflectSet = (v) => {
|
|
148
|
+
currentAsyncConsole = v
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @template T
|
|
153
|
+
* @type {(value: VirtualConsole, fn: () => T | Promise<T>) => Promise<T>}
|
|
154
|
+
*/
|
|
155
|
+
let consoleReflectRun = async (v, fn) => {
|
|
156
|
+
const previousConsole = currentAsyncConsole
|
|
157
|
+
currentAsyncConsole = v
|
|
158
|
+
try {
|
|
159
|
+
const result = fn()
|
|
160
|
+
return await Promise.resolve(result)
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
currentAsyncConsole = previousConsole
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 暴露设置和获取反射逻辑的函数,以完全匹配原始API
|
|
168
|
+
export function setGlobalConsoleReflect(Reflect, ReflectSet, ReflectRun) {
|
|
169
|
+
consoleReflect = () => Reflect(defaultConsole)
|
|
170
|
+
consoleReflectSet = ReflectSet
|
|
171
|
+
consoleReflectRun = ReflectRun
|
|
172
|
+
}
|
|
173
|
+
export function getGlobalConsoleReflect() {
|
|
174
|
+
return {
|
|
175
|
+
Reflect: consoleReflect,
|
|
176
|
+
ReflectSet: consoleReflectSet,
|
|
177
|
+
ReflectRun: consoleReflectRun
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 导出一个代理对象作为全局 console,它将所有操作委托给当前的活动控制台。
|
|
183
|
+
* 这与原始 Node.js 版本的实现完全相同。
|
|
184
|
+
*/
|
|
185
|
+
export const console = globalThis.console = new FullProxy(() => Object.assign({}, globalConsoleAdditionalProperties, consoleReflect()), {
|
|
186
|
+
set: (target, property, value) => {
|
|
187
|
+
target = consoleReflect()
|
|
188
|
+
if (property in target) return Reflect.set(target, property, value)
|
|
189
|
+
globalConsoleAdditionalProperties[property] = value
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
})
|
package/main.mjs
CHANGED
|
@@ -1,197 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export const consoleAsyncStorage = new AsyncLocalStorage()
|
|
11
|
-
const cleanupRegistry = new FinalizationRegistry(cleanupToken => {
|
|
12
|
-
const { stream, listener } = cleanupToken
|
|
13
|
-
stream.off?.('resize', listener)
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 创建一个虚拟控制台,用于捕获输出,同时可以选择性地将输出传递给真实的控制台。
|
|
18
|
-
*
|
|
19
|
-
* @extends {Console}
|
|
20
|
-
*/
|
|
21
|
-
export class VirtualConsole extends Console {
|
|
22
|
-
/**
|
|
23
|
-
* 在新的Async上下文中执行fn,并将fn上下文的控制台替换为此对象。
|
|
24
|
-
* @template T
|
|
25
|
-
* @overload
|
|
26
|
-
* @param {() => T} fn - 在新的Async上下文中执行的函数。
|
|
27
|
-
* @returns {Promise<T>} 返回 fn 函数的 Promise 结果。
|
|
28
|
-
*/
|
|
29
|
-
/**
|
|
30
|
-
* 将当前Async上下文中的控制台替换为此对象。
|
|
31
|
-
* @overload
|
|
32
|
-
* @returns {void}
|
|
33
|
-
*/
|
|
34
|
-
/**
|
|
35
|
-
* 若提供fn,则在新的Async上下文中执行fn,并将fn上下文的控制台替换为此对象。
|
|
36
|
-
* 否则,将当前Async上下文中的控制台替换为此对象。
|
|
37
|
-
* @param {(() => T) | undefined} [fn]
|
|
38
|
-
* @returns {Promise<T> | void}
|
|
39
|
-
*/
|
|
40
|
-
hookAsyncContext(fn) {
|
|
41
|
-
if (fn) return consoleReflectRun(this, fn)
|
|
42
|
-
else consoleReflectSet(this)
|
|
43
|
-
}
|
|
44
|
-
/** @type {string} - 捕获的所有输出 */
|
|
45
|
-
outputs = ''
|
|
46
|
-
|
|
47
|
-
/** @type {object} - 最终合并后的配置项 */
|
|
48
|
-
options
|
|
49
|
-
|
|
50
|
-
/** @type {Console} - 用于 realConsoleOutput 的底层控制台实例 */
|
|
51
|
-
#base_console
|
|
52
|
-
|
|
53
|
-
/** @private @type {string | null} - 用于 freshLine 功能,记录上一次 freshLine 的 ID */
|
|
54
|
-
#loggedFreshLineId = null
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* @param {object} [options={}] - 配置选项。
|
|
58
|
-
* @param {boolean} [options.realConsoleOutput=false] - 如果为 true,则在捕获输出的同时,也调用底层控制台进行实际输出。
|
|
59
|
-
* @param {boolean} [options.recordOutput=true] - 如果为 true,则捕获输出并保存在 outputs 属性中。
|
|
60
|
-
* @param {function(Error): void} [options.error_handler=null] - 一个专门处理单个 Error 对象的错误处理器。
|
|
61
|
-
* @param {Console} [options.base_console=console] - 用于 realConsoleOutput 的底层控制台实例。
|
|
62
|
-
*/
|
|
63
|
-
constructor(options = {}) {
|
|
64
|
-
super(new Writable({ write: () => { } }), new Writable({ write: () => { } }))
|
|
65
|
-
|
|
66
|
-
this.base_console = options.base_console || consoleReflect()
|
|
67
|
-
delete options.base_console
|
|
68
|
-
this.options = {
|
|
69
|
-
realConsoleOutput: false,
|
|
70
|
-
recordOutput: true,
|
|
71
|
-
supportsAnsi: this.#base_console.options?.supportsAnsi || supportsAnsi,
|
|
72
|
-
error_handler: null,
|
|
73
|
-
...options,
|
|
74
|
-
}
|
|
75
|
-
this.freshLine = this.freshLine.bind(this)
|
|
76
|
-
this.clear = this.clear.bind(this)
|
|
77
|
-
for (const method of ['log', 'info', 'warn', 'debug', 'error']) {
|
|
78
|
-
if (!this[method]) continue
|
|
79
|
-
const originalMethod = this[method]
|
|
80
|
-
this[method] = (...args) => {
|
|
81
|
-
if (method == 'error' && this.options.error_handler && args.length === 1 && args[0] instanceof Error) return this.options.error_handler(args[0])
|
|
82
|
-
if (!this.options.realConsoleOutput || this.options.recordOutput) return originalMethod.apply(this, args)
|
|
83
|
-
this.#loggedFreshLineId = null
|
|
84
|
-
return this.#base_console[method](...args)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
get base_console() {
|
|
90
|
-
return this.#base_console
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
set base_console(value) {
|
|
94
|
-
this.#base_console = value
|
|
95
|
-
|
|
96
|
-
const createVirtualStream = (targetStream) => {
|
|
97
|
-
const virtualStream = new Writable({
|
|
98
|
-
write: (chunk, encoding, callback) => {
|
|
99
|
-
this.#loggedFreshLineId = null
|
|
100
|
-
|
|
101
|
-
if (this.options.recordOutput)
|
|
102
|
-
this.outputs += chunk.toString()
|
|
103
|
-
if (this.options.realConsoleOutput)
|
|
104
|
-
targetStream.write(chunk, encoding, callback)
|
|
105
|
-
else
|
|
106
|
-
callback()
|
|
107
|
-
},
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
if (targetStream.isTTY) {
|
|
111
|
-
Object.defineProperties(virtualStream, {
|
|
112
|
-
isTTY: { value: true, configurable: true, writable: false, enumerable: true },
|
|
113
|
-
columns: { get: () => targetStream.columns, configurable: true, enumerable: true },
|
|
114
|
-
rows: { get: () => targetStream.rows, configurable: true, enumerable: true },
|
|
115
|
-
getColorDepth: { get: () => targetStream.getColorDepth.bind(targetStream), configurable: true, enumerable: true },
|
|
116
|
-
hasColors: { get: () => targetStream.hasColors.bind(targetStream), configurable: true, enumerable: true },
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
const virtualStreamRef = new WeakRef(virtualStream)
|
|
120
|
-
|
|
121
|
-
const resizeListener = () => {
|
|
122
|
-
virtualStreamRef.deref()?.emit('resize')
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
targetStream.on?.('resize', resizeListener)
|
|
126
|
-
|
|
127
|
-
cleanupRegistry.register(this, {
|
|
128
|
-
stream: targetStream,
|
|
129
|
-
listener: resizeListener,
|
|
130
|
-
}, this)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return virtualStream
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
this._stdout = createVirtualStream(this.#base_console?._stdout || process.stdout)
|
|
137
|
-
this._stderr = createVirtualStream(this.#base_console?._stderr || process.stderr)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* 在终端中打印一行,如果前一次调用也是具有相同ID的freshLine,
|
|
142
|
-
* 则会覆盖上一行而不是打印新行。
|
|
143
|
-
* @param {string} id - 用于标识可覆盖行的唯一ID。
|
|
144
|
-
* @param {...any} args - 要打印的内容。
|
|
145
|
-
*/
|
|
146
|
-
freshLine(id, ...args) {
|
|
147
|
-
if (this.options.supportsAnsi && this.#loggedFreshLineId === id)
|
|
148
|
-
this._stdout.write(ansiEscapes.cursorUp(1) + ansiEscapes.eraseLine)
|
|
149
|
-
|
|
150
|
-
this.log(...args)
|
|
151
|
-
this.#loggedFreshLineId = id
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
clear() {
|
|
155
|
-
this.#loggedFreshLineId = null
|
|
156
|
-
this.outputs = ''
|
|
157
|
-
if (this.options.realConsoleOutput)
|
|
158
|
-
this.#base_console.clear()
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const originalConsole = globalThis.console
|
|
163
|
-
export const defaultConsole = new VirtualConsole({ base_console: originalConsole, recordOutput: false, realConsoleOutput: true })
|
|
164
|
-
export const globalConsoleAdditionalProperties = {}
|
|
165
|
-
/** @type {() => VirtualConsole} */
|
|
166
|
-
let consoleReflect = () => consoleAsyncStorage.getStore() ?? defaultConsole
|
|
167
|
-
/** @type {(value: VirtualConsole) => void} */
|
|
168
|
-
let consoleReflectSet = (v) => consoleAsyncStorage.enterWith(v)
|
|
169
|
-
/** @type {(value: VirtualConsole, fn: () => T) => Promise<T>} */
|
|
170
|
-
let consoleReflectRun = (v, fn) => consoleAsyncStorage.run(v, fn)
|
|
171
|
-
/**
|
|
172
|
-
* 设置全局控制台反射逻辑
|
|
173
|
-
* @template T
|
|
174
|
-
* @param {(console: Console) => Console} Reflect
|
|
175
|
-
* @param {(value: Console) => void} ReflectSet
|
|
176
|
-
* @param {(value: Console, fn: () => T) => Promise<T>} ReflectRun
|
|
177
|
-
*/
|
|
178
|
-
export function setGlobalConsoleReflect(Reflect, ReflectSet, ReflectRun) {
|
|
179
|
-
consoleReflect = () => Reflect(defaultConsole)
|
|
180
|
-
consoleReflectSet = ReflectSet
|
|
181
|
-
consoleReflectRun = ReflectRun
|
|
182
|
-
}
|
|
183
|
-
export function getGlobalConsoleReflect() {
|
|
184
|
-
return {
|
|
185
|
-
Reflect: consoleReflect,
|
|
186
|
-
ReflectSet: consoleReflectSet,
|
|
187
|
-
ReflectRun: consoleReflectRun
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
export const console = globalThis.console = new FullProxy(() => Object.assign({}, globalConsoleAdditionalProperties, consoleReflect()), {
|
|
191
|
-
set: (target, property, value) => {
|
|
192
|
-
target = consoleReflect()
|
|
193
|
-
if (property in target) return Reflect.set(target, property, value)
|
|
194
|
-
globalConsoleAdditionalProperties[property] = value
|
|
195
|
-
return true
|
|
196
|
-
}
|
|
197
|
-
})
|
|
1
|
+
const module = await import(globalThis.document ? './browser.mjs' : './node.mjs')
|
|
2
|
+
|
|
3
|
+
export const consoleAsyncStorage = module.consoleAsyncStorage
|
|
4
|
+
export const VirtualConsole = module.VirtualConsole
|
|
5
|
+
export const defaultConsole = module.defaultConsole
|
|
6
|
+
export const globalConsoleAdditionalProperties = module.globalConsoleAdditionalProperties
|
|
7
|
+
export const setGlobalConsoleReflect = module.setGlobalConsoleReflect
|
|
8
|
+
export const getGlobalConsoleReflect = module.getGlobalConsoleReflect
|
|
9
|
+
export const console = module.console
|
package/node.mjs
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import { Console } from 'node:console'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { Writable } from 'node:stream'
|
|
5
|
+
|
|
6
|
+
import ansiEscapes from 'ansi-escapes'
|
|
7
|
+
import { FullProxy } from 'full-proxy'
|
|
8
|
+
import supportsAnsi from 'supports-ansi'
|
|
9
|
+
|
|
10
|
+
export const consoleAsyncStorage = new AsyncLocalStorage()
|
|
11
|
+
const cleanupRegistry = new FinalizationRegistry(cleanupToken => {
|
|
12
|
+
const { stream, listener } = cleanupToken
|
|
13
|
+
stream.off?.('resize', listener)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 创建一个虚拟控制台,用于捕获输出,同时可以选择性地将输出传递给真实的控制台。
|
|
18
|
+
*
|
|
19
|
+
* @extends {Console}
|
|
20
|
+
*/
|
|
21
|
+
export class VirtualConsole extends Console {
|
|
22
|
+
/**
|
|
23
|
+
* 在新的Async上下文中执行fn,并将fn上下文的控制台替换为此对象。
|
|
24
|
+
* @template T
|
|
25
|
+
* @overload
|
|
26
|
+
* @param {() => T} fn - 在新的Async上下文中执行的函数。
|
|
27
|
+
* @returns {Promise<T>} 返回 fn 函数的 Promise 结果。
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* 将当前Async上下文中的控制台替换为此对象。
|
|
31
|
+
* @overload
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* 若提供fn,则在新的Async上下文中执行fn,并将fn上下文的控制台替换为此对象。
|
|
36
|
+
* 否则,将当前Async上下文中的控制台替换为此对象。
|
|
37
|
+
* @param {(() => T) | undefined} [fn]
|
|
38
|
+
* @returns {Promise<T> | void}
|
|
39
|
+
*/
|
|
40
|
+
hookAsyncContext(fn) {
|
|
41
|
+
if (fn) return consoleReflectRun(this, fn)
|
|
42
|
+
else consoleReflectSet(this)
|
|
43
|
+
}
|
|
44
|
+
/** @type {string} - 捕获的所有输出 */
|
|
45
|
+
outputs = ''
|
|
46
|
+
|
|
47
|
+
/** @type {object} - 最终合并后的配置项 */
|
|
48
|
+
options
|
|
49
|
+
|
|
50
|
+
/** @type {Console} - 用于 realConsoleOutput 的底层控制台实例 */
|
|
51
|
+
#base_console
|
|
52
|
+
|
|
53
|
+
/** @private @type {string | null} - 用于 freshLine 功能,记录上一次 freshLine 的 ID */
|
|
54
|
+
#loggedFreshLineId = null
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} [options={}] - 配置选项。
|
|
58
|
+
* @param {boolean} [options.realConsoleOutput=false] - 如果为 true,则在捕获输出的同时,也调用底层控制台进行实际输出。
|
|
59
|
+
* @param {boolean} [options.recordOutput=true] - 如果为 true,则捕获输出并保存在 outputs 属性中。
|
|
60
|
+
* @param {function(Error): void} [options.error_handler=null] - 一个专门处理单个 Error 对象的错误处理器。
|
|
61
|
+
* @param {Console} [options.base_console=console] - 用于 realConsoleOutput 的底层控制台实例。
|
|
62
|
+
*/
|
|
63
|
+
constructor(options = {}) {
|
|
64
|
+
super(new Writable({ write: () => { } }), new Writable({ write: () => { } }))
|
|
65
|
+
|
|
66
|
+
this.base_console = options.base_console || consoleReflect()
|
|
67
|
+
delete options.base_console
|
|
68
|
+
this.options = {
|
|
69
|
+
realConsoleOutput: false,
|
|
70
|
+
recordOutput: true,
|
|
71
|
+
supportsAnsi: this.#base_console.options?.supportsAnsi || supportsAnsi,
|
|
72
|
+
error_handler: null,
|
|
73
|
+
...options,
|
|
74
|
+
}
|
|
75
|
+
this.freshLine = this.freshLine.bind(this)
|
|
76
|
+
this.clear = this.clear.bind(this)
|
|
77
|
+
for (const method of ['log', 'info', 'warn', 'debug', 'error']) {
|
|
78
|
+
if (!this[method]) continue
|
|
79
|
+
const originalMethod = this[method]
|
|
80
|
+
this[method] = (...args) => {
|
|
81
|
+
if (method == 'error' && this.options.error_handler && args.length === 1 && args[0] instanceof Error) return this.options.error_handler(args[0])
|
|
82
|
+
if (!this.options.realConsoleOutput || this.options.recordOutput) return originalMethod.apply(this, args)
|
|
83
|
+
this.#loggedFreshLineId = null
|
|
84
|
+
return this.#base_console[method](...args)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get base_console() {
|
|
90
|
+
return this.#base_console
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
set base_console(value) {
|
|
94
|
+
this.#base_console = value
|
|
95
|
+
|
|
96
|
+
const createVirtualStream = (targetStream) => {
|
|
97
|
+
const virtualStream = new Writable({
|
|
98
|
+
write: (chunk, encoding, callback) => {
|
|
99
|
+
this.#loggedFreshLineId = null
|
|
100
|
+
|
|
101
|
+
if (this.options.recordOutput)
|
|
102
|
+
this.outputs += chunk.toString()
|
|
103
|
+
if (this.options.realConsoleOutput)
|
|
104
|
+
targetStream.write(chunk, encoding, callback)
|
|
105
|
+
else
|
|
106
|
+
callback()
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (targetStream.isTTY) {
|
|
111
|
+
Object.defineProperties(virtualStream, {
|
|
112
|
+
isTTY: { value: true, configurable: true, writable: false, enumerable: true },
|
|
113
|
+
columns: { get: () => targetStream.columns, configurable: true, enumerable: true },
|
|
114
|
+
rows: { get: () => targetStream.rows, configurable: true, enumerable: true },
|
|
115
|
+
getColorDepth: { get: () => targetStream.getColorDepth.bind(targetStream), configurable: true, enumerable: true },
|
|
116
|
+
hasColors: { get: () => targetStream.hasColors.bind(targetStream), configurable: true, enumerable: true },
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const virtualStreamRef = new WeakRef(virtualStream)
|
|
120
|
+
|
|
121
|
+
const resizeListener = () => {
|
|
122
|
+
virtualStreamRef.deref()?.emit('resize')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
targetStream.on?.('resize', resizeListener)
|
|
126
|
+
|
|
127
|
+
cleanupRegistry.register(this, {
|
|
128
|
+
stream: targetStream,
|
|
129
|
+
listener: resizeListener,
|
|
130
|
+
}, this)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return virtualStream
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this._stdout = createVirtualStream(this.#base_console?._stdout || process.stdout)
|
|
137
|
+
this._stderr = createVirtualStream(this.#base_console?._stderr || process.stderr)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 在终端中打印一行,如果前一次调用也是具有相同ID的freshLine,
|
|
142
|
+
* 则会覆盖上一行而不是打印新行。
|
|
143
|
+
* @param {string} id - 用于标识可覆盖行的唯一ID。
|
|
144
|
+
* @param {...any} args - 要打印的内容。
|
|
145
|
+
*/
|
|
146
|
+
freshLine(id, ...args) {
|
|
147
|
+
if (this.options.supportsAnsi && this.#loggedFreshLineId === id)
|
|
148
|
+
this._stdout.write(ansiEscapes.cursorUp(1) + ansiEscapes.eraseLine)
|
|
149
|
+
|
|
150
|
+
this.log(...args)
|
|
151
|
+
this.#loggedFreshLineId = id
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
clear() {
|
|
155
|
+
this.#loggedFreshLineId = null
|
|
156
|
+
this.outputs = ''
|
|
157
|
+
if (this.options.realConsoleOutput)
|
|
158
|
+
this.#base_console.clear()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const originalConsole = globalThis.console
|
|
163
|
+
export const defaultConsole = new VirtualConsole({ base_console: originalConsole, recordOutput: false, realConsoleOutput: true })
|
|
164
|
+
export const globalConsoleAdditionalProperties = {}
|
|
165
|
+
/** @type {() => VirtualConsole} */
|
|
166
|
+
let consoleReflect = () => consoleAsyncStorage.getStore() ?? defaultConsole
|
|
167
|
+
/** @type {(value: VirtualConsole) => void} */
|
|
168
|
+
let consoleReflectSet = (v) => consoleAsyncStorage.enterWith(v)
|
|
169
|
+
/** @type {(value: VirtualConsole, fn: () => T) => Promise<T>} */
|
|
170
|
+
let consoleReflectRun = (v, fn) => consoleAsyncStorage.run(v, fn)
|
|
171
|
+
/**
|
|
172
|
+
* 设置全局控制台反射逻辑
|
|
173
|
+
* @template T
|
|
174
|
+
* @param {(console: Console) => Console} Reflect
|
|
175
|
+
* @param {(value: Console) => void} ReflectSet
|
|
176
|
+
* @param {(value: Console, fn: () => T) => Promise<T>} ReflectRun
|
|
177
|
+
*/
|
|
178
|
+
export function setGlobalConsoleReflect(Reflect, ReflectSet, ReflectRun) {
|
|
179
|
+
consoleReflect = () => Reflect(defaultConsole)
|
|
180
|
+
consoleReflectSet = ReflectSet
|
|
181
|
+
consoleReflectRun = ReflectRun
|
|
182
|
+
}
|
|
183
|
+
export function getGlobalConsoleReflect() {
|
|
184
|
+
return {
|
|
185
|
+
Reflect: consoleReflect,
|
|
186
|
+
ReflectSet: consoleReflectSet,
|
|
187
|
+
ReflectRun: consoleReflectRun
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export const console = globalThis.console = new FullProxy(() => Object.assign({}, globalConsoleAdditionalProperties, consoleReflect()), {
|
|
191
|
+
set: (target, property, value) => {
|
|
192
|
+
target = consoleReflect()
|
|
193
|
+
if (property in target) return Reflect.set(target, property, value)
|
|
194
|
+
globalConsoleAdditionalProperties[property] = value
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
})
|