@innei/pretty-logger-core 0.3.0

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.
@@ -0,0 +1,320 @@
1
+ import { getColor } from './color'
2
+ import { stripAnsi } from './string'
3
+
4
+ export type BoxBorderStyle = {
5
+ /**
6
+ * Top left corner
7
+ * @example `┌`
8
+ * @example `╔`
9
+ * @example `╓`
10
+ */
11
+ tl: string
12
+ /**
13
+ * Top right corner
14
+ * @example `┐`
15
+ * @example `╗`
16
+ * @example `╖`
17
+ */
18
+ tr: string
19
+ /**
20
+ * Bottom left corner
21
+ * @example `└`
22
+ * @example `╚`
23
+ * @example `╙`
24
+ */
25
+ bl: string
26
+ /**
27
+ * Bottom right corner
28
+ * @example `┘`
29
+ * @example `╝`
30
+ * @example `╜`
31
+ */
32
+ br: string
33
+ /**
34
+ * Horizontal line
35
+ * @example `─`
36
+ * @example `═`
37
+ * @example `─`
38
+ */
39
+ h: string
40
+ /**
41
+ * Vertical line
42
+ * @example `│`
43
+ * @example `║`
44
+ * @example `║`
45
+ */
46
+ v: string
47
+ }
48
+
49
+ const boxStylePresets: Record<string, BoxBorderStyle> = {
50
+ solid: {
51
+ tl: '┌',
52
+ tr: '┐',
53
+ bl: '└',
54
+ br: '┘',
55
+ h: '─',
56
+ v: '│',
57
+ },
58
+ double: {
59
+ tl: '╔',
60
+ tr: '╗',
61
+ bl: '╚',
62
+ br: '╝',
63
+ h: '═',
64
+ v: '║',
65
+ },
66
+ doubleSingle: {
67
+ tl: '╓',
68
+ tr: '╖',
69
+ bl: '╙',
70
+ br: '╜',
71
+ h: '─',
72
+ v: '║',
73
+ },
74
+ doubleSingleRounded: {
75
+ tl: '╭',
76
+ tr: '╮',
77
+ bl: '╰',
78
+ br: '╯',
79
+ h: '─',
80
+ v: '║',
81
+ },
82
+ singleThick: {
83
+ tl: '┏',
84
+ tr: '┓',
85
+ bl: '┗',
86
+ br: '┛',
87
+ h: '━',
88
+ v: '┃',
89
+ },
90
+ singleDouble: {
91
+ tl: '╒',
92
+ tr: '╕',
93
+ bl: '╘',
94
+ br: '╛',
95
+ h: '═',
96
+ v: '│',
97
+ },
98
+ singleDoubleRounded: {
99
+ tl: '╭',
100
+ tr: '╮',
101
+ bl: '╰',
102
+ br: '╯',
103
+ h: '═',
104
+ v: '│',
105
+ },
106
+ rounded: {
107
+ tl: '╭',
108
+ tr: '╮',
109
+ bl: '╰',
110
+ br: '╯',
111
+ h: '─',
112
+ v: '│',
113
+ },
114
+ }
115
+
116
+ export type BoxStyle = {
117
+ /**
118
+ * The border color
119
+ * @default 'white'
120
+ */
121
+ borderColor:
122
+ | 'black'
123
+ | 'red'
124
+ | 'green'
125
+ | 'yellow'
126
+ | 'blue'
127
+ | 'magenta'
128
+ | 'cyan'
129
+ | 'white'
130
+ | 'gray'
131
+ | 'blackBright'
132
+ | 'redBright'
133
+ | 'greenBright'
134
+ | 'yellowBright'
135
+ | 'blueBright'
136
+ | 'magentaBright'
137
+ | 'cyanBright'
138
+ | 'whiteBright'
139
+
140
+ /**
141
+ * The border style
142
+ * @default 'solid'
143
+ * @example 'single-double-rounded'
144
+ * @example
145
+ * ```ts
146
+ * {
147
+ * tl: '┌',
148
+ * tr: '┐',
149
+ * bl: '└',
150
+ * br: '┘',
151
+ * h: '─',
152
+ * v: '│',
153
+ * }
154
+ * ```
155
+ */
156
+ borderStyle: BoxBorderStyle | keyof typeof boxStylePresets
157
+
158
+ /**
159
+ * The vertical alignment of the text
160
+ * @default 'center'
161
+ */
162
+ valign: 'top' | 'center' | 'bottom'
163
+
164
+ /**
165
+ * The padding of the box
166
+ * @default 2
167
+ */
168
+ padding: number
169
+
170
+ /**
171
+ * The left margin of the box
172
+ * @default 1
173
+ */
174
+ marginLeft: number
175
+
176
+ /**
177
+ * The top margin of the box
178
+ * @default 1
179
+ */
180
+ marginTop: number
181
+
182
+ /**
183
+ * The top margin of the box
184
+ * @default 1
185
+ */
186
+ marginBottom: number
187
+ }
188
+
189
+ /**
190
+ * The border options of the box
191
+ */
192
+ export type BoxOpts = {
193
+ /**
194
+ * Title that will be displayed on top of the box
195
+ * @example 'Hello World'
196
+ * @example 'Hello {name}'
197
+ */
198
+ title?: string
199
+
200
+ style?: Partial<BoxStyle>
201
+ }
202
+
203
+ const defaultStyle: BoxStyle = {
204
+ borderColor: 'white',
205
+ borderStyle: 'rounded',
206
+ valign: 'center',
207
+ padding: 2,
208
+ marginLeft: 1,
209
+ marginTop: 1,
210
+ marginBottom: 1,
211
+ }
212
+
213
+ export function box(text: string, _opts: BoxOpts = {}) {
214
+ const opts = {
215
+ ..._opts,
216
+ style: {
217
+ ...defaultStyle,
218
+ ..._opts.style,
219
+ },
220
+ }
221
+
222
+ // Split the text into lines
223
+ const textLines = text.split('\n')
224
+
225
+ // Create the box
226
+ const boxLines = []
227
+
228
+ // Get the characters for the box and colorize
229
+ const _color = getColor(opts.style.borderColor)
230
+ const borderStyle = {
231
+ ...(typeof opts.style.borderStyle === 'string'
232
+ ? boxStylePresets[
233
+ opts.style.borderStyle as keyof typeof boxStylePresets
234
+ ] || boxStylePresets.solid
235
+ : opts.style.borderStyle),
236
+ }
237
+ if (_color) {
238
+ for (const key in borderStyle) {
239
+ borderStyle[key as keyof typeof borderStyle] = _color(
240
+ borderStyle[key as keyof typeof borderStyle],
241
+ )
242
+ }
243
+ }
244
+
245
+ // Calculate the width and height of the box
246
+ const paddingOffset =
247
+ opts.style.padding % 2 === 0 ? opts.style.padding : opts.style.padding + 1
248
+ const height = textLines.length + paddingOffset
249
+ const width =
250
+ Math.max(...textLines.map((line) => line.length)) + paddingOffset
251
+ const widthOffset = width + paddingOffset
252
+
253
+ const leftSpace =
254
+ opts.style.marginLeft > 0 ? ' '.repeat(opts.style.marginLeft) : ''
255
+
256
+ // Top line
257
+ if (opts.style.marginTop > 0) {
258
+ boxLines.push(''.repeat(opts.style.marginTop))
259
+ }
260
+ // Include the title if it exists with borders
261
+ if (opts.title) {
262
+ const left = borderStyle.h.repeat(
263
+ Math.floor((width - stripAnsi(opts.title).length) / 2),
264
+ )
265
+ const right = borderStyle.h.repeat(
266
+ width -
267
+ stripAnsi(opts.title).length -
268
+ stripAnsi(left).length +
269
+ paddingOffset,
270
+ )
271
+ boxLines.push(
272
+ `${leftSpace}${borderStyle.tl}${left}${opts.title}${right}${borderStyle.tr}`,
273
+ )
274
+ } else {
275
+ boxLines.push(
276
+ `${leftSpace}${borderStyle.tl}${borderStyle.h.repeat(widthOffset)}${
277
+ borderStyle.tr
278
+ }`,
279
+ )
280
+ }
281
+
282
+ // Middle lines
283
+ const valignOffset =
284
+ opts.style.valign === 'center'
285
+ ? Math.floor((height - textLines.length) / 2)
286
+ : opts.style.valign === 'top'
287
+ ? height - textLines.length - paddingOffset
288
+ : height - textLines.length
289
+
290
+ for (let i = 0; i < height; i++) {
291
+ if (i < valignOffset || i >= valignOffset + textLines.length) {
292
+ // Empty line
293
+ boxLines.push(
294
+ `${leftSpace}${borderStyle.v}${' '.repeat(widthOffset)}${
295
+ borderStyle.v
296
+ }`,
297
+ )
298
+ } else {
299
+ // Text line
300
+ const line = textLines[i - valignOffset]
301
+ const left = ' '.repeat(paddingOffset)
302
+ const right = ' '.repeat(width - stripAnsi(line).length)
303
+ boxLines.push(
304
+ `${leftSpace}${borderStyle.v}${left}${line}${right}${borderStyle.v}`,
305
+ )
306
+ }
307
+ }
308
+
309
+ // Bottom line
310
+ boxLines.push(
311
+ `${leftSpace}${borderStyle.bl}${borderStyle.h.repeat(widthOffset)}${
312
+ borderStyle.br
313
+ }`,
314
+ )
315
+ if (opts.style.marginBottom > 0) {
316
+ boxLines.push(''.repeat(opts.style.marginBottom))
317
+ }
318
+
319
+ return boxLines.join('\n')
320
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Based on https://github.com/jorgebucaran/colorette
3
+ * Read LICENSE file for more information
4
+ * https://github.com/jorgebucaran/colorette/blob/20fc196d07d0f87c61e0256eadd7831c79b24108/index.js
5
+ */
6
+
7
+ import * as tty from 'node:tty'
8
+
9
+ // TODO: Migrate to std-env
10
+ const {
11
+ env = {},
12
+ argv = [],
13
+ platform = '',
14
+ } = typeof process === 'undefined' ? {} : process
15
+ const isDisabled = 'NO_COLOR' in env || argv.includes('--no-color')
16
+ const isForced = 'FORCE_COLOR' in env || argv.includes('--color')
17
+ const isWindows = platform === 'win32'
18
+ const isDumbTerminal = env.TERM === 'dumb'
19
+ const isCompatibleTerminal =
20
+ tty && tty.isatty && tty.isatty(1) && env.TERM && !isDumbTerminal
21
+ const isCI =
22
+ 'CI' in env &&
23
+ ('GITHUB_ACTIONS' in env || 'GITLAB_CI' in env || 'CIRCLECI' in env)
24
+ const isColorSupported =
25
+ !isDisabled &&
26
+ (isForced || (isWindows && !isDumbTerminal) || isCompatibleTerminal || isCI)
27
+
28
+ function replaceClose(
29
+ index: number,
30
+ string: string,
31
+ close: string,
32
+ replace: string,
33
+ head = string.slice(0, Math.max(0, index)) + replace,
34
+ tail = string.slice(Math.max(0, index + close.length)),
35
+ next = tail.indexOf(close),
36
+ ): string {
37
+ return head + (next < 0 ? tail : replaceClose(next, tail, close, replace))
38
+ }
39
+
40
+ function clearBleed(
41
+ index: number,
42
+ string: string,
43
+ open: string,
44
+ close: string,
45
+ replace: string,
46
+ ) {
47
+ return index < 0
48
+ ? open + string + close
49
+ : open + replaceClose(index, string, close, replace) + close
50
+ }
51
+
52
+ function filterEmpty(
53
+ open: string,
54
+ close: string,
55
+ replace = open,
56
+ at = open.length + 1,
57
+ ) {
58
+ return (string: string) =>
59
+ string || !(string === '' || string === undefined)
60
+ ? clearBleed(`${string}`.indexOf(close, at), string, open, close, replace)
61
+ : ''
62
+ }
63
+
64
+ function init(open: number, close: number, replace?: string) {
65
+ return filterEmpty(`\u001B[${open}m`, `\u001B[${close}m`, replace)
66
+ }
67
+
68
+ const colorDefs = {
69
+ reset: init(0, 0),
70
+ bold: init(1, 22, '\u001B[22m\u001B[1m'),
71
+ dim: init(2, 22, '\u001B[22m\u001B[2m'),
72
+ italic: init(3, 23),
73
+ underline: init(4, 24),
74
+ inverse: init(7, 27),
75
+ hidden: init(8, 28),
76
+ strikethrough: init(9, 29),
77
+ black: init(30, 39),
78
+ red: init(31, 39),
79
+ green: init(32, 39),
80
+ yellow: init(33, 39),
81
+ blue: init(34, 39),
82
+ magenta: init(35, 39),
83
+ cyan: init(36, 39),
84
+ white: init(37, 39),
85
+ gray: init(90, 39),
86
+ bgBlack: init(40, 49),
87
+ bgRed: init(41, 49),
88
+ bgGreen: init(42, 49),
89
+ bgYellow: init(43, 49),
90
+ bgBlue: init(44, 49),
91
+ bgMagenta: init(45, 49),
92
+ bgCyan: init(46, 49),
93
+ bgWhite: init(47, 49),
94
+ blackBright: init(90, 39),
95
+ redBright: init(91, 39),
96
+ greenBright: init(92, 39),
97
+ yellowBright: init(93, 39),
98
+ blueBright: init(94, 39),
99
+ magentaBright: init(95, 39),
100
+ cyanBright: init(96, 39),
101
+ whiteBright: init(97, 39),
102
+ bgBlackBright: init(100, 49),
103
+ bgRedBright: init(101, 49),
104
+ bgGreenBright: init(102, 49),
105
+ bgYellowBright: init(103, 49),
106
+ bgBlueBright: init(104, 49),
107
+ bgMagentaBright: init(105, 49),
108
+ bgCyanBright: init(106, 49),
109
+ bgWhiteBright: init(107, 49),
110
+ }
111
+
112
+ export type ColorName = keyof typeof colorDefs
113
+ export type ColorFunction = (text: string | number) => string
114
+
115
+ function createColors(useColor = isColorSupported) {
116
+ return useColor
117
+ ? colorDefs
118
+ : Object.fromEntries(Object.keys(colorDefs).map((key) => [key, String]))
119
+ }
120
+
121
+ export const colors = createColors() as Record<ColorName, ColorFunction>
122
+
123
+ export function getColor(
124
+ color: ColorName,
125
+ fallback: ColorName = 'reset',
126
+ ): ColorFunction {
127
+ return colors[color] || colors[fallback]
128
+ }
129
+
130
+ export function colorize(color: ColorName, text: string | number): string {
131
+ return getColor(color)(text)
132
+ }
@@ -0,0 +1,12 @@
1
+ import { sep } from 'node:path'
2
+
3
+ export function parseStack(stack: string) {
4
+ const cwd = process.cwd() + sep
5
+
6
+ const lines = stack
7
+ .split('\n')
8
+ .splice(1)
9
+ .map((l) => l.trim().replace('file://', '').replace(cwd, ''))
10
+
11
+ return lines
12
+ }
@@ -0,0 +1,22 @@
1
+ export function isPlainObject(obj: any) {
2
+ return Object.prototype.toString.call(obj) === '[object Object]'
3
+ }
4
+
5
+ export function isLogObj(arg: any) {
6
+ // Should be plain object
7
+ if (!isPlainObject(arg)) {
8
+ return false
9
+ }
10
+
11
+ // Should contains either 'message' or 'args' field
12
+ if (!arg.message && !arg.args) {
13
+ return false
14
+ }
15
+
16
+ // Handle non-standard error objects
17
+ if (arg.stack) {
18
+ return false
19
+ }
20
+
21
+ return true
22
+ }
@@ -0,0 +1,9 @@
1
+ import type { WriteStream } from 'node:fs'
2
+
3
+ export function writeStream(
4
+ data: any,
5
+ stream: NodeJS.WriteStream | WriteStream,
6
+ ) {
7
+ const write = (stream as any).__write || stream.write
8
+ return write.call(stream, data)
9
+ }
@@ -0,0 +1,64 @@
1
+ const ansiRegex = [
2
+ '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
3
+ '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
4
+ ].join('|')
5
+
6
+ export function stripAnsi(text: string) {
7
+ return text.replace(new RegExp(ansiRegex, 'g'), '')
8
+ }
9
+
10
+ export function centerAlign(str: string, len: number, space = ' ') {
11
+ const free = len - str.length
12
+ if (free <= 0) {
13
+ return str
14
+ }
15
+ const freeLeft = Math.floor(free / 2)
16
+ let _str = ''
17
+ for (let i = 0; i < len; i++) {
18
+ _str +=
19
+ i < freeLeft || i >= freeLeft + str.length ? space : str[i - freeLeft]
20
+ }
21
+ return _str
22
+ }
23
+
24
+ export function rightAlign(str: string, len: number, space = ' ') {
25
+ const free = len - str.length
26
+ if (free <= 0) {
27
+ return str
28
+ }
29
+ let _str = ''
30
+ for (let i = 0; i < len; i++) {
31
+ _str += i < free ? space : str[i - free]
32
+ }
33
+ return _str
34
+ }
35
+
36
+ export function leftAlign(str: string, len: number, space = ' ') {
37
+ let _str = ''
38
+ for (let i = 0; i < len; i++) {
39
+ _str += i < str.length ? str[i] : space
40
+ }
41
+ return _str
42
+ }
43
+
44
+ export function align(
45
+ alignment: 'left' | 'right' | 'center',
46
+ str: string,
47
+ len: number,
48
+ space = ' ',
49
+ ) {
50
+ switch (alignment) {
51
+ case 'left': {
52
+ return leftAlign(str, len, space)
53
+ }
54
+ case 'right': {
55
+ return rightAlign(str, len, space)
56
+ }
57
+ case 'center': {
58
+ return centerAlign(str, len, space)
59
+ }
60
+ default: {
61
+ return str
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,16 @@
1
+ export function isUnicodeSupported() {
2
+ if (process.platform !== 'win32') {
3
+ return process.env.TERM !== 'linux' // Linux console (kernel)
4
+ }
5
+
6
+ return (
7
+ Boolean(process.env.WT_SESSION) || // Windows Terminal
8
+ Boolean(process.env.TERMINUS_SUBLIME) || // Terminus (<0.2.27)
9
+ process.env.ConEmuTask === '{cmd::Cmder}' || // ConEmu and cmder
10
+ process.env.TERM_PROGRAM === 'Terminus-Sublime' ||
11
+ process.env.TERM_PROGRAM === 'vscode' ||
12
+ process.env.TERM === 'xterm-256color' ||
13
+ process.env.TERM === 'alacritty' ||
14
+ process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm'
15
+ )
16
+ }
@@ -0,0 +1,9 @@
1
+ export * from './utils/box'
2
+ export * from './utils/color'
3
+ export {
4
+ stripAnsi,
5
+ centerAlign,
6
+ rightAlign,
7
+ leftAlign,
8
+ align,
9
+ } from './utils/string'
@@ -0,0 +1,36 @@
1
+ import { isDevelopment } from 'std-env'
2
+ import type { ConsolaOptions, ConsolaReporter } from './consola'
3
+ import type { FileReporterConfig } from './consola/reporters/file'
4
+
5
+ import { createConsola, LogLevels } from './consola'
6
+ import { FancyReporter } from './consola/reporters/fancy'
7
+ import { FileReporter } from './consola/reporters/file'
8
+ import {
9
+ SubscriberReporter,
10
+ wrapperSubscribers,
11
+ } from './consola/reporters/subscriber'
12
+
13
+ export interface LoggerConsolaOptions extends Partial<ConsolaOptions> {
14
+ writeToFile?: FileReporterConfig
15
+ }
16
+
17
+ export const createLoggerConsola = (options?: LoggerConsolaOptions) => {
18
+ const reporters: ConsolaReporter[] = [
19
+ new FancyReporter(),
20
+ new SubscriberReporter(),
21
+ ]
22
+ if (options?.writeToFile) {
23
+ reporters.push(new FileReporter(options.writeToFile))
24
+ }
25
+ const consola = createConsola({
26
+ formatOptions: {
27
+ date: true,
28
+ },
29
+
30
+ reporters,
31
+ level: isDevelopment ? LogLevels.trace : LogLevels.info,
32
+ ...options,
33
+ })
34
+
35
+ return wrapperSubscribers(consola)
36
+ }