@renpwn/simplelog 0.0.1
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/LICENSE +21 -0
- package/README.md +295 -0
- package/package.json +41 -0
- package/src/FileSink.js +51 -0
- package/src/Formatter.js +22 -0
- package/src/Levels.js +20 -0
- package/src/Logger.js +117 -0
- package/src/Progress/ProgressManager.js +62 -0
- package/src/Progress/ProgressRenderer.js +48 -0
- package/src/Stringify.js +26 -0
- package/src/Time.js +17 -0
- package/src/index.js +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RENPWN
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# simpleLog
|
|
2
|
+
|
|
3
|
+
> Lightweight, opinionated, **TTY-aware logger** for Node.js with progress bar, file output, safe stringify, and zero dependencies.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@renpwn/simplelog)
|
|
6
|
+
[](https://www.npmjs.com/package/@renpwn/simplelog)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## โจ Features
|
|
12
|
+
|
|
13
|
+
- ๐จ Colored log levels (log, debug, info, warn, error)
|
|
14
|
+
- ๐ง Safe stringify (object โ JSON, anti crash, truncate)
|
|
15
|
+
- ๐ Timestamp with locale (`id`, `en`)
|
|
16
|
+
- ๐ File logging (TXT / JSONL + auto backup)
|
|
17
|
+
- ๐ Multi progress bar (TTY-aware, auto redraw)
|
|
18
|
+
- ๐งน Non-TTY & CI safe
|
|
19
|
+
- โก Zero dependencies
|
|
20
|
+
- ๐งฉ Modular & audit-friendly
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ๐ฆ Installation
|
|
25
|
+
|
|
26
|
+
### NPM
|
|
27
|
+
```bash
|
|
28
|
+
npm install @renpwn/simplelog
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Yarn
|
|
32
|
+
```bash
|
|
33
|
+
yarn add @renpwn/simplelog
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Git Clone
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/renpwn/simpleLog.git
|
|
39
|
+
cd simpleLog
|
|
40
|
+
npm install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## ๐ Quick Start (Minimal)
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { simpleLog } from '@renpwn/simplelog'
|
|
49
|
+
|
|
50
|
+
const log = simpleLog()
|
|
51
|
+
|
|
52
|
+
log.log('hello')
|
|
53
|
+
log.info('info message')
|
|
54
|
+
log.warn('warning')
|
|
55
|
+
log.error('error')
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## ๐ง Full Usage Example
|
|
61
|
+
|
|
62
|
+
### 1๏ธโฃ Logger dengan Level, Warna & Waktu
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { simpleLog } from '@renpwn/simplelog'
|
|
66
|
+
|
|
67
|
+
const log = simpleLog({
|
|
68
|
+
level: 'debug', // log | debug | info | warn | error | silent
|
|
69
|
+
color: true, // enable ANSI color
|
|
70
|
+
time: {
|
|
71
|
+
locale: 'id', // id | en
|
|
72
|
+
position: 'prefix' // prefix | suffix
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
log.debug('Debug message')
|
|
77
|
+
log.info('Server started')
|
|
78
|
+
log.warn('Memory usage high')
|
|
79
|
+
log.error({ code: 500, msg: 'Fatal error' })
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
๐ **Keterangan**
|
|
83
|
+
- `level` โ filter minimum level yang ditampilkan
|
|
84
|
+
- `color` โ otomatis nonaktif jika non-TTY
|
|
85
|
+
- `time` โ format waktu ringkas & konsisten
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### 2๏ธโฃ Safe Stringify & Truncate
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
const log = simpleLog({
|
|
93
|
+
truncate: {
|
|
94
|
+
maxLength: 200
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
log.info({
|
|
99
|
+
veryLongData: 'x'.repeat(1000)
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
๐ Object akan di-`JSON.stringify`, dan otomatis dipotong jika terlalu panjang.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### 3๏ธโฃ File Logging (TXT & JSONL)
|
|
108
|
+
|
|
109
|
+
#### TXT (default)
|
|
110
|
+
```js
|
|
111
|
+
const log = simpleLog({
|
|
112
|
+
file: {
|
|
113
|
+
path: 'logs/app.log'
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
log.info('App started')
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Output:
|
|
121
|
+
```
|
|
122
|
+
[2026-01-20T07:21:10.120Z] INFO App started
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### JSONL
|
|
126
|
+
```js
|
|
127
|
+
const log = simpleLog({
|
|
128
|
+
file: {
|
|
129
|
+
path: 'logs/app.json',
|
|
130
|
+
format: 'json'
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Output:
|
|
136
|
+
```json
|
|
137
|
+
{"time":"2026-01-20T07:21:10.120Z","level":"info","message":"App started"}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
๐ File write aman dengan auto-backup `.bak`.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### 4๏ธโฃ Progress Bar (Multi Slot)
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
const log = simpleLog({
|
|
148
|
+
progress: {
|
|
149
|
+
slots: [
|
|
150
|
+
['Scraping', { color: 'cyan' }],
|
|
151
|
+
['DB Queue', 'auto']
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
let i = 0
|
|
157
|
+
const timer = setInterval(() => {
|
|
158
|
+
i++
|
|
159
|
+
log.updateProgress('Scraping', i, 10, 'fetching...')
|
|
160
|
+
log.updateProgress('DB Queue', i * 2, 20)
|
|
161
|
+
|
|
162
|
+
if (i >= 10) {
|
|
163
|
+
clearInterval(timer)
|
|
164
|
+
log.info('All jobs finished')
|
|
165
|
+
}
|
|
166
|
+
}, 300)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
๐ **Catatan**
|
|
170
|
+
- Progress hanya muncul di TTY
|
|
171
|
+
- Log biasa akan membersihkan progress lalu merender ulang
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### 5๏ธโฃ Custom Progress Theme
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
const log = simpleLog({
|
|
179
|
+
progress: {
|
|
180
|
+
slots: ['Download'],
|
|
181
|
+
theme: {
|
|
182
|
+
size: 30,
|
|
183
|
+
filled: 'โ',
|
|
184
|
+
empty: 'โ',
|
|
185
|
+
left: '[',
|
|
186
|
+
right: ']',
|
|
187
|
+
style: { color: 'green' }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## ๐งฉ API Ringkas
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
log.log(...args)
|
|
199
|
+
log.debug(...args)
|
|
200
|
+
log.info(...args)
|
|
201
|
+
log.warn(...args)
|
|
202
|
+
log.error(...args)
|
|
203
|
+
|
|
204
|
+
log.updateProgress(name, cur, total, text?)
|
|
205
|
+
log.removeProgress(name)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## ๐ง Architecture
|
|
211
|
+
|
|
212
|
+
### console.log
|
|
213
|
+
```
|
|
214
|
+
console.log()
|
|
215
|
+
โ
|
|
216
|
+
stdout
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
โ No level
|
|
220
|
+
โ No file
|
|
221
|
+
โ No progress
|
|
222
|
+
โ No safety
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### simpleLog
|
|
227
|
+
```
|
|
228
|
+
simpleLog()
|
|
229
|
+
โ
|
|
230
|
+
โโ Levels (filter)
|
|
231
|
+
โโ Time formatter
|
|
232
|
+
โโ Safe stringify
|
|
233
|
+
โโ ANSI formatter
|
|
234
|
+
โโ FileSink (txt / jsonl)
|
|
235
|
+
โ
|
|
236
|
+
โโ ProgressManager
|
|
237
|
+
โโ ProgressRenderer
|
|
238
|
+
โ
|
|
239
|
+
stdout (TTY aware)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## ๐ Project Structure
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
simplelog/
|
|
248
|
+
โโ package.json
|
|
249
|
+
โโ src/
|
|
250
|
+
โโ index.js # entry point (simpleLog)
|
|
251
|
+
โ
|
|
252
|
+
โโ Logger.js # logger utama
|
|
253
|
+
โโ Levels.js # level & style
|
|
254
|
+
โโ Formatter.js # ANSI formatter
|
|
255
|
+
โโ Stringify.js # stringify + truncate
|
|
256
|
+
โโ Time.js # time formatter
|
|
257
|
+
โโ FileSink.js # file logging
|
|
258
|
+
โ
|
|
259
|
+
โโ Progress/
|
|
260
|
+
โโ ProgressManager.js # progress state
|
|
261
|
+
โโ ProgressRenderer.js # progress bar renderer
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## ๐ง Design Philosophy
|
|
267
|
+
|
|
268
|
+
- Small core
|
|
269
|
+
- No dependency
|
|
270
|
+
- Predictable output
|
|
271
|
+
- Audit friendly
|
|
272
|
+
- Library-first design
|
|
273
|
+
|
|
274
|
+
Cocok untuk:
|
|
275
|
+
- CLI tools
|
|
276
|
+
- Bot WhatsApp / Telegram
|
|
277
|
+
- Scraper
|
|
278
|
+
- Worker / queue
|
|
279
|
+
- Base library (`simpleStore`, `simpleFetch`, dll)
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## ๐ Links
|
|
284
|
+
|
|
285
|
+
- GitHub
|
|
286
|
+
https://github.com/renpwn/simpleLog
|
|
287
|
+
|
|
288
|
+
- NPM
|
|
289
|
+
https://www.npmjs.com/package/@renpwn/simplelog
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## ๐ License
|
|
294
|
+
|
|
295
|
+
MIT ยฉ RenPwn
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@renpwn/simplelog",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Lightweight, safe, audit-friendly logger for CLI tools and long-running Node.js processes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"logger",
|
|
15
|
+
"logging",
|
|
16
|
+
"cli",
|
|
17
|
+
"terminal",
|
|
18
|
+
"progress-bar",
|
|
19
|
+
"jsonl",
|
|
20
|
+
"ndjson",
|
|
21
|
+
"tty",
|
|
22
|
+
"node"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "RenPwn",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/renpwn/simpleLog.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/renpwn/simpleLog",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/renpwn/simpleLog/issues"
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=16"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/FileSink.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
export class FileSink {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
if (!opts.path) {
|
|
7
|
+
this.enabled = false
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
this.enabled = true
|
|
12
|
+
this.format = opts.format || 'txt' // txt | json (JSONL)
|
|
13
|
+
this.backup = opts.backup !== false
|
|
14
|
+
this.basePath = path.resolve(process.cwd(), opts.path)
|
|
15
|
+
|
|
16
|
+
fs.mkdirSync(path.dirname(this.basePath), { recursive: true })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
backupFile(file) {
|
|
20
|
+
if (!this.backup || !fs.existsSync(file)) return
|
|
21
|
+
fs.copyFileSync(file, file + '.bak')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
restoreFile(file) {
|
|
25
|
+
const bak = file + '.bak'
|
|
26
|
+
if (fs.existsSync(bak)) fs.copyFileSync(bak, file)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
write(level, message) {
|
|
30
|
+
if (!this.enabled) return
|
|
31
|
+
const time = new Date().toISOString()
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (this.format === 'json') {
|
|
35
|
+
this.backupFile(this.basePath)
|
|
36
|
+
fs.appendFileSync(
|
|
37
|
+
this.basePath,
|
|
38
|
+
JSON.stringify({ time, level, message }) + '\n'
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fs.appendFileSync(
|
|
44
|
+
this.basePath,
|
|
45
|
+
`[${time}] ${level.toUpperCase()} ${message}\n`
|
|
46
|
+
)
|
|
47
|
+
} catch {
|
|
48
|
+
this.restoreFile(this.basePath)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/Formatter.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const COLORS = {
|
|
2
|
+
black:30, red:31, green:32, yellow:33,
|
|
3
|
+
blue:34, magenta:35, cyan:36, white:37
|
|
4
|
+
}
|
|
5
|
+
const BG = {
|
|
6
|
+
black:40, red:41, green:42, yellow:43,
|
|
7
|
+
blue:44, magenta:45, cyan:46, white:47
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function format(text, style, tty = true) {
|
|
11
|
+
if (!tty || !style) return text
|
|
12
|
+
|
|
13
|
+
const codes = []
|
|
14
|
+
if (style.bold) codes.push(1)
|
|
15
|
+
if (style.dim) codes.push(2)
|
|
16
|
+
if (style.color && COLORS[style.color]) codes.push(COLORS[style.color])
|
|
17
|
+
if (style.bg && BG[style.bg]) codes.push(BG[style.bg])
|
|
18
|
+
|
|
19
|
+
return codes.length
|
|
20
|
+
? `\x1b[${codes.join(';')}m${text}\x1b[0m`
|
|
21
|
+
: text
|
|
22
|
+
}
|
package/src/Levels.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const LEVELS = {
|
|
2
|
+
log: 0,
|
|
3
|
+
debug: 10,
|
|
4
|
+
info: 20,
|
|
5
|
+
warn: 30,
|
|
6
|
+
error: 40,
|
|
7
|
+
silent: 99
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const LEVEL_STYLE = {
|
|
11
|
+
log: { },
|
|
12
|
+
debug: { color: 'cyan', dim: true },
|
|
13
|
+
info: { color: 'green' },
|
|
14
|
+
warn: { color: 'yellow', bold: true },
|
|
15
|
+
error: { color: 'white', bg: 'red', bold: true }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeLevel(level = 'log') {
|
|
19
|
+
return LEVELS[level] !== undefined ? level : 'log'
|
|
20
|
+
}
|
package/src/Logger.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { LEVELS, LEVEL_STYLE, normalizeLevel } from './Levels.js'
|
|
2
|
+
import { createStringifier } from './Stringify.js'
|
|
3
|
+
import { formatTime } from './Time.js'
|
|
4
|
+
import { format } from './Formatter.js'
|
|
5
|
+
import { FileSink } from './FileSink.js'
|
|
6
|
+
import { ProgressManager } from './Progress/ProgressManager.js'
|
|
7
|
+
|
|
8
|
+
export class Logger {
|
|
9
|
+
constructor(opts = {}) {
|
|
10
|
+
this.level = normalizeLevel(opts.level)
|
|
11
|
+
this.color = !!opts.color
|
|
12
|
+
this.tty = process.stdout.isTTY && !process.env.CI
|
|
13
|
+
|
|
14
|
+
this.time = !!opts.time
|
|
15
|
+
this.timeLocale = opts.time?.locale || 'id'
|
|
16
|
+
this.timePos = opts.time?.position || 'prefix'
|
|
17
|
+
|
|
18
|
+
this.stringify = createStringifier(
|
|
19
|
+
opts.truncate || { maxLength: opts.maxLength }
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
this.file = new FileSink(opts.file || {})
|
|
23
|
+
|
|
24
|
+
this.progress = opts.progress
|
|
25
|
+
? new ProgressManager(opts.progress.slots || [], opts.progress.theme)
|
|
26
|
+
: null
|
|
27
|
+
|
|
28
|
+
this.lastProgressLines = 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
allow(type) {
|
|
32
|
+
return LEVELS[type] >= LEVELS[this.level]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
style(type, user) {
|
|
36
|
+
if (!this.color) return user
|
|
37
|
+
return { ...LEVEL_STYLE[type], ...user }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* ================= PROGRESS API ================= */
|
|
41
|
+
|
|
42
|
+
updateProgress(name, cur, total, text) {
|
|
43
|
+
if (!this.progress) return
|
|
44
|
+
this.progress.update(name, cur, total, text)
|
|
45
|
+
this.renderProgress()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
removeProgress(name) {
|
|
49
|
+
if (!this.progress) return
|
|
50
|
+
this.progress.remove(name)
|
|
51
|
+
this.renderProgress()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ================= RENDER CONTROL ================= */
|
|
55
|
+
|
|
56
|
+
clearProgress() {
|
|
57
|
+
if (!this.progress || !this.tty || this.lastProgressLines === 0) return
|
|
58
|
+
|
|
59
|
+
process.stdout.write(`\x1b[${this.lastProgressLines}A`)
|
|
60
|
+
for (let i = 0; i < this.lastProgressLines; i++) {
|
|
61
|
+
process.stdout.write('\x1b[2K')
|
|
62
|
+
process.stdout.write('\x1b[1B')
|
|
63
|
+
}
|
|
64
|
+
process.stdout.write(`\x1b[${this.lastProgressLines}A`)
|
|
65
|
+
this.lastProgressLines = 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
renderProgress() {
|
|
69
|
+
if (!this.progress || !this.tty) return
|
|
70
|
+
|
|
71
|
+
const snapshot = this.progress.snapshot()
|
|
72
|
+
if (!snapshot.length) return
|
|
73
|
+
|
|
74
|
+
this.clearProgress()
|
|
75
|
+
|
|
76
|
+
process.stdout.write('\n')
|
|
77
|
+
for (const p of snapshot) {
|
|
78
|
+
process.stdout.write(`${p.line}\n`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.lastProgressLines = snapshot.length + 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ================= CORE WRITE ================= */
|
|
85
|
+
|
|
86
|
+
write(type, args, style) {
|
|
87
|
+
if (!this.allow(type)) return
|
|
88
|
+
|
|
89
|
+
const msg = args.map(this.stringify.toStr).join(' ')
|
|
90
|
+
const t = this.time ? formatTime(this.timeLocale) : null
|
|
91
|
+
|
|
92
|
+
const out =
|
|
93
|
+
t && this.timePos === 'suffix'
|
|
94
|
+
? `${msg} | ${t}`
|
|
95
|
+
: t
|
|
96
|
+
? `${t} ${msg}`
|
|
97
|
+
: msg
|
|
98
|
+
|
|
99
|
+
if (this.progress) {
|
|
100
|
+
this.clearProgress()
|
|
101
|
+
process.stdout.write(
|
|
102
|
+
format(out, this.style(type, style), this.tty) + '\n'
|
|
103
|
+
)
|
|
104
|
+
this.renderProgress()
|
|
105
|
+
} else {
|
|
106
|
+
console.log(format(out, this.style(type, style), this.tty))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.file.write(type, out)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
log(...a) { this.write('log', a) }
|
|
113
|
+
debug(...a) { this.write('debug', a) }
|
|
114
|
+
info(...a) { this.write('info', a) }
|
|
115
|
+
warn(...a) { this.write('warn', a) }
|
|
116
|
+
error(...a) { this.write('error', a) }
|
|
117
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ProgressRenderer } from './ProgressRenderer.js'
|
|
2
|
+
|
|
3
|
+
export class ProgressManager {
|
|
4
|
+
constructor(slots = [], theme = {}) {
|
|
5
|
+
this.slots = slots
|
|
6
|
+
this.state = new Map()
|
|
7
|
+
this.renderer = new ProgressRenderer(theme)
|
|
8
|
+
this.theme = theme?.style || null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
update(name, cur, total, text = "") {
|
|
12
|
+
this.state.set(name, {
|
|
13
|
+
cur,
|
|
14
|
+
total: total < cur ? cur : total,
|
|
15
|
+
text,
|
|
16
|
+
start: Date.now()
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
remove(name) {
|
|
21
|
+
this.state.delete(name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
clear() {
|
|
25
|
+
this.state.clear()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
snapshot() {
|
|
29
|
+
const s = a => Array.isArray(a) ? a : []
|
|
30
|
+
|
|
31
|
+
return this.slots.map(item => {
|
|
32
|
+
const name = s(item)[0] || item
|
|
33
|
+
const vStyle = s(item)[1] || { }
|
|
34
|
+
|
|
35
|
+
const v = this.state.get(name) || { cur: 0, total: 0, text: '' }
|
|
36
|
+
|
|
37
|
+
const percent = v.total
|
|
38
|
+
? Math.floor((v.cur / v.total) * 100)
|
|
39
|
+
: 0
|
|
40
|
+
|
|
41
|
+
const elapsed = v.start
|
|
42
|
+
? Math.floor((Date.now() - v.start) / 1000)
|
|
43
|
+
: 0
|
|
44
|
+
|
|
45
|
+
const eta =
|
|
46
|
+
v.cur && v.total && elapsed
|
|
47
|
+
? Math.floor((elapsed / v.cur) * (v.total - v.cur))
|
|
48
|
+
: null
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name,
|
|
52
|
+
cur: v.cur,
|
|
53
|
+
total: v.total,
|
|
54
|
+
percent,
|
|
55
|
+
elapsed,
|
|
56
|
+
eta,
|
|
57
|
+
//text: v.text,
|
|
58
|
+
line: this.renderer.render(name, v.cur, v.total, v.text, vStyle)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { format } from '../Formatter.js'
|
|
2
|
+
|
|
3
|
+
export class ProgressRenderer {
|
|
4
|
+
constructor({ size = 20, filled = 'โ', empty = 'โ', style = {}, left = '[', right = ']'} = {}) {
|
|
5
|
+
this.size = size
|
|
6
|
+
this.filled = filled
|
|
7
|
+
this.empty = empty
|
|
8
|
+
this.style = style
|
|
9
|
+
this.left = left
|
|
10
|
+
this.right = right
|
|
11
|
+
this.tty = process.stdout.isTTY && !process.env.CI
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
styleResolve(style, current, percent){
|
|
15
|
+
if(!style) return null
|
|
16
|
+
|
|
17
|
+
//auto style
|
|
18
|
+
if (style === 'auto')
|
|
19
|
+
return percent >= 85
|
|
20
|
+
? { color: 'red', bold: true }
|
|
21
|
+
: percent >= 45
|
|
22
|
+
? { color: 'yellow' }
|
|
23
|
+
: { color: 'blue' }
|
|
24
|
+
|
|
25
|
+
//format from style dual or single
|
|
26
|
+
style = Array.isArray(style) ? (current > 0 && style.length > 1 ? style[1] : style[0]) : style
|
|
27
|
+
return style && typeof style === 'object' && Object.keys(style).length ? style : null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
render(name, cur, total, text, style = {}) {
|
|
31
|
+
if (!total) total = 1
|
|
32
|
+
const percent = Math.floor((cur / total) * 100)
|
|
33
|
+
const filled = Math.min(
|
|
34
|
+
this.size,
|
|
35
|
+
Math.max(0, Math.floor(percent / 100 * this.size))
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
style = this.styleResolve(style, cur, percent)
|
|
39
|
+
this.style = this.styleResolve(this.style, cur)
|
|
40
|
+
|
|
41
|
+
const left = format(`${name} ${this.left}`, this.style, this.tty)
|
|
42
|
+
const right = format(`${this.right} ${percent}% ${text}`, this.style, this.tty)
|
|
43
|
+
const bar = format(this.filled.repeat(filled) + this.empty.repeat(this.size - filled), style, this.tty)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
return left + bar + right
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/Stringify.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function createStringifier(opts = {}) {
|
|
2
|
+
const enabled = opts.enabled !== false
|
|
3
|
+
const max = Number.isInteger(opts.maxLength) ? opts.maxLength : 500
|
|
4
|
+
|
|
5
|
+
function truncate(str) {
|
|
6
|
+
if (!enabled) return str
|
|
7
|
+
if (str.length > max) {
|
|
8
|
+
return str.slice(0, max) +
|
|
9
|
+
`... [TRUNCATED ${str.length - max} chars]`
|
|
10
|
+
}
|
|
11
|
+
return str
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toStr(v) {
|
|
15
|
+
if (typeof v === 'object' && v !== null) {
|
|
16
|
+
try {
|
|
17
|
+
return truncate(JSON.stringify(v, null, 2))
|
|
18
|
+
} catch {
|
|
19
|
+
return truncate(String(v))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return truncate(String(v))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { toStr }
|
|
26
|
+
}
|
package/src/Time.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const LOCALES = {
|
|
2
|
+
id: {
|
|
3
|
+
days: ["MIN","SEN","SEL","RAB","KAM","JUM","SAB"],
|
|
4
|
+
mons: ["JAN","FEB","MAR","APR","MEI","JUN","JUL","AGS","SEP","OKT","NOV","DES"]
|
|
5
|
+
},
|
|
6
|
+
en: {
|
|
7
|
+
days: ["SUN","MON","TUE","WED","THU","FRI","SAT"],
|
|
8
|
+
mons: ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"]
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatTime(locale = 'id', date = new Date()) {
|
|
13
|
+
const l = LOCALES[locale] || LOCALES.id
|
|
14
|
+
const d = date
|
|
15
|
+
|
|
16
|
+
return `${l.days[d.getDay()]}|${String(d.getDate()).padStart(2,'0')}.${l.mons[d.getMonth()]}|${d.toTimeString().split(' ')[0]}`
|
|
17
|
+
}
|