wukong-progress 0.1.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 +219 -0
- package/README.zh-CN.md +217 -0
- package/dist/index.mjs +159 -0
- package/package.json +104 -0
- package/src/bar.mjs +67 -0
- package/src/color.mjs +12 -0
- package/src/group.mjs +15 -0
- package/src/index.mjs +1 -0
- package/src/json.mjs +16 -0
- package/src/multibar.mjs +69 -0
- package/src/utils.mjs +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2025] [杨琼]
|
|
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,219 @@
|
|
|
1
|
+
# wukong-progress
|
|
2
|
+
|
|
3
|
+
🎨 A Node.js / ESM style CLI progress bar library that supports:
|
|
4
|
+
|
|
5
|
+
- Single / multiple progress bars
|
|
6
|
+
- Group / Stage / prefix
|
|
7
|
+
- Concurrent task API (wrap async functions)
|
|
8
|
+
- Automatic terminal width adaptation
|
|
9
|
+
- Optional colored output (chalk, auto fallback)
|
|
10
|
+
- JSON fallback (for non-TTY environments)
|
|
11
|
+
- Full ESM + Node.js 18+ compatible
|
|
12
|
+
- Works on Windows / Linux / macOS
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
## English | [简体中文](./README.zh-CN.md)
|
|
16
|
+
---
|
|
17
|
+
## 🚀 Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
yarn add wukong-progress chalk
|
|
21
|
+
# or
|
|
22
|
+
npm install wukong-progress chalk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## ⚡️ Basic Usage
|
|
28
|
+
|
|
29
|
+
### Single Progress Bar
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import chalk from "chalk";
|
|
33
|
+
import { createMultiBar } from "wukong-progress";
|
|
34
|
+
|
|
35
|
+
const mb = createMultiBar();
|
|
36
|
+
const bar = mb.create(100, {
|
|
37
|
+
prefix: chalk.cyan("Build"),
|
|
38
|
+
format: "Build [:bar] :percent :current/:total",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
async function run() {
|
|
42
|
+
for (let i = 0; i <= 100; i++) {
|
|
43
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
44
|
+
bar.tick();
|
|
45
|
+
}
|
|
46
|
+
mb.stop();
|
|
47
|
+
console.log(chalk.green("\nDone!\n"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
run();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Multiple Progress Bars
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import chalk from "chalk";
|
|
57
|
+
import { createMultiBar } from "wukong-progress";
|
|
58
|
+
|
|
59
|
+
const mb = createMultiBar();
|
|
60
|
+
const build = mb.create(100, {
|
|
61
|
+
prefix: chalk.blue("Build"),
|
|
62
|
+
format: "Build [:bar] :percent",
|
|
63
|
+
});
|
|
64
|
+
const test = mb.create(50, {
|
|
65
|
+
prefix: chalk.magenta("Test"),
|
|
66
|
+
format: "Test [:bar] :percent",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
async function run() {
|
|
70
|
+
for (let i = 0; i <= 100; i++) {
|
|
71
|
+
await new Promise((r) => setTimeout(r, 15));
|
|
72
|
+
if (i <= 50) test.tick();
|
|
73
|
+
build.tick();
|
|
74
|
+
}
|
|
75
|
+
mb.stop();
|
|
76
|
+
console.log(chalk.green("\nAll tasks done!\n"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
run();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### Group / Stage / prefix
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import chalk from "chalk";
|
|
88
|
+
import { createMultiBar } from "wukong-progress";
|
|
89
|
+
|
|
90
|
+
const mb = createMultiBar();
|
|
91
|
+
const buildGroup = mb.group("Build Group");
|
|
92
|
+
buildGroup.create(50, {
|
|
93
|
+
prefix: chalk.blue("Compile"),
|
|
94
|
+
format: "Compile [:bar] :percent",
|
|
95
|
+
});
|
|
96
|
+
buildGroup.create(30, {
|
|
97
|
+
prefix: chalk.cyan("Bundle"),
|
|
98
|
+
format: "Bundle [:bar] :percent",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const testGroup = mb.group("Test Group");
|
|
102
|
+
testGroup.create(20, {
|
|
103
|
+
prefix: chalk.magenta("Unit"),
|
|
104
|
+
format: "Unit [:bar] :percent",
|
|
105
|
+
});
|
|
106
|
+
testGroup.create(10, {
|
|
107
|
+
prefix: chalk.yellow("E2E"),
|
|
108
|
+
format: "E2E [:bar] :percent",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
async function run() {
|
|
112
|
+
const allTasks = [...buildGroup.bars, ...testGroup.bars];
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < 50; i++) {
|
|
115
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
116
|
+
allTasks.forEach((bar) => {
|
|
117
|
+
if (!bar.state.complete) bar.tick();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
mb.stop();
|
|
122
|
+
console.log(chalk.green("\nGroups completed!\n"));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
run();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### JSON Fallback (non-TTY / CI)
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
import { Writable } from "node:stream";
|
|
134
|
+
import { createMultiBar } from "wukong-progress";
|
|
135
|
+
|
|
136
|
+
let out = "";
|
|
137
|
+
const stream = new Writable({
|
|
138
|
+
write(chunk, _, cb) {
|
|
139
|
+
out += chunk;
|
|
140
|
+
cb();
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const mb = createMultiBar({ stream, json: true });
|
|
145
|
+
const bar = mb.create(5, { prefix: "JSON" });
|
|
146
|
+
|
|
147
|
+
async function run() {
|
|
148
|
+
for (let i = 0; i <= 5; i++) {
|
|
149
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
150
|
+
bar.tick();
|
|
151
|
+
}
|
|
152
|
+
mb.stop();
|
|
153
|
+
|
|
154
|
+
console.log("JSON fallback output:");
|
|
155
|
+
console.log(out);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
run();
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 🎨 Colored Output (optional)
|
|
164
|
+
|
|
165
|
+
- Use `chalk` to color prefixes and bar output
|
|
166
|
+
- Auto fallback to plain text if chalk is not installed
|
|
167
|
+
- Example:
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
prefix: chalk.green('Build'),
|
|
171
|
+
format: chalk.green('Build [:bar] :percent')
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 📂 Examples Folder
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
node examples/index.mjs
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- Interactive selection of examples:
|
|
183
|
+
|
|
184
|
+
- Single Bar
|
|
185
|
+
- Multi Bar
|
|
186
|
+
- Group / Stage
|
|
187
|
+
- JSON Fallback
|
|
188
|
+
|
|
189
|
+
- Fully compatible with Windows / Linux / macOS
|
|
190
|
+
- All examples use async/await + chalk colored output
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## ⚡️ Testing
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Node.js native test
|
|
198
|
+
yarn test:node
|
|
199
|
+
|
|
200
|
+
# Vitest snapshot test
|
|
201
|
+
yarn test:vitest
|
|
202
|
+
|
|
203
|
+
# Run all tests
|
|
204
|
+
yarn test
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- ✅ Node:test for progress logic
|
|
208
|
+
- ✅ Vitest snapshot for rendering stability
|
|
209
|
+
- ✅ Supports mock TTY / ANSI strip / JSON fallback
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 💻 Use Cases
|
|
214
|
+
|
|
215
|
+
- CLI tools
|
|
216
|
+
- Automation scripts
|
|
217
|
+
- GitHub Actions / CI
|
|
218
|
+
- Concurrent task visualization
|
|
219
|
+
- Progress visualization + JSON logging for analytics
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
|
|
2
|
+
# wukong-progress
|
|
3
|
+
|
|
4
|
+
🎨 Node.js / ESM 风格的 CLI 进度条库,支持:
|
|
5
|
+
|
|
6
|
+
- 单条 / 多条进度条
|
|
7
|
+
- Group / Stage / prefix
|
|
8
|
+
- 并发任务 API(wrap async fn)
|
|
9
|
+
- 自动适配终端宽度
|
|
10
|
+
- 彩色渲染(chalk,可选,自动降级)
|
|
11
|
+
- JSON fallback(非 TTY)
|
|
12
|
+
- 完全 ESM + Node.js 18+ 兼容
|
|
13
|
+
- 可在 Windows / Linux / macOS 使用
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
## 中文 | [English](./README.md)
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🚀 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
yarn add wukong-progress chalk
|
|
23
|
+
# or
|
|
24
|
+
npm install wukong-progress chalk
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ⚡️ 基本用法
|
|
30
|
+
|
|
31
|
+
### 单条进度条
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import chalk from 'chalk'
|
|
35
|
+
import { createMultiBar } from 'wukong-progress'
|
|
36
|
+
|
|
37
|
+
const mb = createMultiBar()
|
|
38
|
+
const bar = mb.create(100, { prefix: chalk.cyan('Build'), format: 'Build [:bar] :percent :current/:total' })
|
|
39
|
+
|
|
40
|
+
async function run() {
|
|
41
|
+
for (let i = 0; i <= 100; i++) {
|
|
42
|
+
await new Promise(r => setTimeout(r, 20))
|
|
43
|
+
bar.tick()
|
|
44
|
+
}
|
|
45
|
+
mb.stop()
|
|
46
|
+
console.log(chalk.green('\nDone!\n'))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
run()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 多条进度条
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import chalk from 'chalk'
|
|
56
|
+
import { createMultiBar } from 'wukong-progress'
|
|
57
|
+
|
|
58
|
+
const mb = createMultiBar()
|
|
59
|
+
const build = mb.create(100, { prefix: chalk.blue('Build'), format: 'Build [:bar] :percent' })
|
|
60
|
+
const test = mb.create(50, { prefix: chalk.magenta('Test'), format: 'Test [:bar] :percent' })
|
|
61
|
+
|
|
62
|
+
async function run() {
|
|
63
|
+
for (let i = 0; i <= 100; i++) {
|
|
64
|
+
await new Promise(r => setTimeout(r, 15))
|
|
65
|
+
if (i <= 50) test.tick()
|
|
66
|
+
build.tick()
|
|
67
|
+
}
|
|
68
|
+
mb.stop()
|
|
69
|
+
console.log(chalk.green('\nAll tasks done!\n'))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
run()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### Group / Stage / prefix
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import chalk from 'chalk'
|
|
81
|
+
import { createMultiBar } from 'wukong-progress'
|
|
82
|
+
|
|
83
|
+
const mb = createMultiBar()
|
|
84
|
+
const buildGroup = mb.group('Build Group')
|
|
85
|
+
buildGroup.create(50, { prefix: chalk.blue('Compile'), format: 'Compile [:bar] :percent' })
|
|
86
|
+
buildGroup.create(30, { prefix: chalk.cyan('Bundle'), format: 'Bundle [:bar] :percent' })
|
|
87
|
+
|
|
88
|
+
const testGroup = mb.group('Test Group')
|
|
89
|
+
testGroup.create(20, { prefix: chalk.magenta('Unit'), format: 'Unit [:bar] :percent' })
|
|
90
|
+
testGroup.create(10, { prefix: chalk.yellow('E2E'), format: 'E2E [:bar] :percent' })
|
|
91
|
+
|
|
92
|
+
async function run() {
|
|
93
|
+
const allTasks = [...buildGroup.bars, ...testGroup.bars]
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < 50; i++) {
|
|
96
|
+
await new Promise(r => setTimeout(r, 20))
|
|
97
|
+
allTasks.forEach(bar => {
|
|
98
|
+
if (!bar.state.complete) bar.tick()
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
mb.stop()
|
|
103
|
+
console.log(chalk.green('\nGroups completed!\n'))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
run()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### JSON fallback(非 TTY 或 CI)
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
import { Writable } from 'node:stream'
|
|
115
|
+
import { createMultiBar } from 'wukong-progress'
|
|
116
|
+
|
|
117
|
+
let out = ''
|
|
118
|
+
const stream = new Writable({
|
|
119
|
+
write(chunk, _, cb) {
|
|
120
|
+
out += chunk
|
|
121
|
+
cb()
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const mb = createMultiBar({ stream, json: true })
|
|
126
|
+
const bar = mb.create(5, { prefix: 'JSON' })
|
|
127
|
+
|
|
128
|
+
async function run() {
|
|
129
|
+
for (let i = 0; i <= 5; i++) {
|
|
130
|
+
await new Promise(r => setTimeout(r, 20))
|
|
131
|
+
bar.tick()
|
|
132
|
+
}
|
|
133
|
+
mb.stop()
|
|
134
|
+
|
|
135
|
+
console.log('JSON fallback output:')
|
|
136
|
+
console.log(out)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
run()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 🎨 彩色渲染(可选)
|
|
145
|
+
|
|
146
|
+
- 使用 `chalk` 可以给 prefix、bar 和提示上色
|
|
147
|
+
|
|
148
|
+
- 不依赖彩色也能降级到普通文本
|
|
149
|
+
|
|
150
|
+
- 示例:
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
prefix: chalk.green('Build'),
|
|
155
|
+
format: chalk.green('Build [:bar] :percent')
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 📂 Examples 文件夹
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
node examples/index.mjs
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- 交互式选择运行示例:
|
|
167
|
+
|
|
168
|
+
- Single Bar
|
|
169
|
+
|
|
170
|
+
- Multi Bar
|
|
171
|
+
|
|
172
|
+
- Group / Stage
|
|
173
|
+
|
|
174
|
+
- JSON Fallback
|
|
175
|
+
|
|
176
|
+
- Windows / Linux / macOS 全平台兼容
|
|
177
|
+
|
|
178
|
+
- 所有示例都用 async/await + chalk 彩色渲染
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## ⚡️ 测试
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Node.js 原生测试
|
|
187
|
+
yarn test:node
|
|
188
|
+
|
|
189
|
+
# Vitest snapshot 测试
|
|
190
|
+
yarn test:vitest
|
|
191
|
+
|
|
192
|
+
# 全部测试
|
|
193
|
+
yarn test
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- ✅ Node:test 测试进度条逻辑
|
|
197
|
+
|
|
198
|
+
- ✅ Vitest snapshot 测试渲染稳定性
|
|
199
|
+
|
|
200
|
+
- ✅ 支持 mock TTY / ANSI strip / JSON fallback
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 💻 适用场景
|
|
206
|
+
|
|
207
|
+
- CLI 工具
|
|
208
|
+
|
|
209
|
+
- 自动化脚本
|
|
210
|
+
|
|
211
|
+
- GitHub Actions / CI
|
|
212
|
+
|
|
213
|
+
- 多任务并发显示
|
|
214
|
+
|
|
215
|
+
- 可视化进度 / JSON 输出结合日志分析
|
|
216
|
+
|
|
217
|
+
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/color.mjs
|
|
2
|
+
var chalk = null;
|
|
3
|
+
try {
|
|
4
|
+
const mod = await import("chalk");
|
|
5
|
+
chalk = mod.default;
|
|
6
|
+
} catch {
|
|
7
|
+
}
|
|
8
|
+
var color = {
|
|
9
|
+
bar: (s) => chalk ? chalk.cyan(s) : s,
|
|
10
|
+
dim: (s) => chalk ? chalk.dim(s) : s,
|
|
11
|
+
percent: (s) => chalk ? chalk.green(s) : s
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/utils.mjs
|
|
15
|
+
var clamp = (n, min, max) => Math.min(Math.max(n, min), max);
|
|
16
|
+
var formatTime = (sec) => {
|
|
17
|
+
if (!isFinite(sec)) return "--";
|
|
18
|
+
if (sec < 60) return `${sec.toFixed(1)}s`;
|
|
19
|
+
const m = Math.floor(sec / 60);
|
|
20
|
+
const s = Math.floor(sec % 60);
|
|
21
|
+
return `${m}m${s}s`;
|
|
22
|
+
};
|
|
23
|
+
var termWidth = (stream) => stream.columns || 80;
|
|
24
|
+
|
|
25
|
+
// src/bar.mjs
|
|
26
|
+
function createBar(ctx, opts) {
|
|
27
|
+
const state = {
|
|
28
|
+
total: opts.total,
|
|
29
|
+
current: 0,
|
|
30
|
+
start: Date.now(),
|
|
31
|
+
format: opts.format,
|
|
32
|
+
minWidth: opts.minWidth ?? 10,
|
|
33
|
+
prefix: opts.prefix ?? "",
|
|
34
|
+
hideOnComplete: opts.hideOnComplete ?? false,
|
|
35
|
+
complete: false
|
|
36
|
+
};
|
|
37
|
+
function update(n) {
|
|
38
|
+
state.current = clamp(n, 0, state.total);
|
|
39
|
+
if (state.current >= state.total) {
|
|
40
|
+
state.complete = true;
|
|
41
|
+
}
|
|
42
|
+
ctx.render();
|
|
43
|
+
}
|
|
44
|
+
function tick(n = 1) {
|
|
45
|
+
update(state.current + n);
|
|
46
|
+
}
|
|
47
|
+
function render(width) {
|
|
48
|
+
const elapsed = (Date.now() - state.start) / 1e3;
|
|
49
|
+
const percent = state.current / state.total;
|
|
50
|
+
const staticLen = state.format.replace(":bar", "").replace(":percent", "100%").replace(":current", state.total).replace(":total", state.total).replace(":eta", "00s").length;
|
|
51
|
+
const barWidth = Math.max(
|
|
52
|
+
state.minWidth,
|
|
53
|
+
width - staticLen - state.prefix.length - 4
|
|
54
|
+
);
|
|
55
|
+
const filled = Math.round(barWidth * percent);
|
|
56
|
+
const bar = color.bar("\u2588".repeat(filled)) + color.dim("\u2591".repeat(barWidth - filled));
|
|
57
|
+
const rate = state.current / elapsed;
|
|
58
|
+
const eta = rate ? (state.total - state.current) / rate : Infinity;
|
|
59
|
+
return (state.prefix ? `${state.prefix} ` : "") + state.format.replace(":bar", bar).replace(":percent", color.percent(`${Math.round(percent * 100)}%`)).replace(":current", state.current).replace(":total", state.total).replace(":elapsed", formatTime(elapsed)).replace(":eta", formatTime(eta));
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
state,
|
|
63
|
+
tick,
|
|
64
|
+
update,
|
|
65
|
+
render
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/group.mjs
|
|
70
|
+
function createGroup(ctx, name) {
|
|
71
|
+
const bars = [];
|
|
72
|
+
function create(total, opts = {}) {
|
|
73
|
+
const bar = ctx._createBar({
|
|
74
|
+
...opts,
|
|
75
|
+
prefix: opts.prefix ?? name,
|
|
76
|
+
total
|
|
77
|
+
});
|
|
78
|
+
bars.push(bar);
|
|
79
|
+
return bar;
|
|
80
|
+
}
|
|
81
|
+
return { create, bars };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/json.mjs
|
|
85
|
+
function renderJSON(stream, bars) {
|
|
86
|
+
const payload = {
|
|
87
|
+
type: "progress",
|
|
88
|
+
bars: bars.map((b) => ({
|
|
89
|
+
name: b.state.prefix,
|
|
90
|
+
current: b.state.current,
|
|
91
|
+
total: b.state.total,
|
|
92
|
+
percent: Math.round(
|
|
93
|
+
b.state.current / b.state.total * 100
|
|
94
|
+
),
|
|
95
|
+
complete: b.state.complete
|
|
96
|
+
}))
|
|
97
|
+
};
|
|
98
|
+
stream.write(`${JSON.stringify(payload)}
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/multibar.mjs
|
|
103
|
+
function createMultiBar({
|
|
104
|
+
stream = process.stderr,
|
|
105
|
+
clearOnComplete = false,
|
|
106
|
+
json = false
|
|
107
|
+
} = {}) {
|
|
108
|
+
const bars = [];
|
|
109
|
+
let lines = 0;
|
|
110
|
+
let active = true;
|
|
111
|
+
const isJSON = json || !stream.isTTY;
|
|
112
|
+
let ctx = null;
|
|
113
|
+
function group(name) {
|
|
114
|
+
return createGroup(ctx, name);
|
|
115
|
+
}
|
|
116
|
+
function render() {
|
|
117
|
+
if (!active) return;
|
|
118
|
+
if (isJSON) {
|
|
119
|
+
renderJSON(stream, bars);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (lines > 0) {
|
|
123
|
+
stream.write(`\x1B[${lines}A`);
|
|
124
|
+
}
|
|
125
|
+
const width = termWidth(stream);
|
|
126
|
+
const visible = bars.filter(
|
|
127
|
+
(b) => !(b.state.complete && b.state.hideOnComplete)
|
|
128
|
+
);
|
|
129
|
+
const output = visible.map((b) => b.render(width));
|
|
130
|
+
stream.write(`${output.join("\n")}
|
|
131
|
+
`);
|
|
132
|
+
lines = output.length;
|
|
133
|
+
}
|
|
134
|
+
function stop() {
|
|
135
|
+
active = false;
|
|
136
|
+
if (!isJSON && clearOnComplete) {
|
|
137
|
+
stream.write(`\x1B[${lines}A\x1B[J`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function _createBar(opts) {
|
|
141
|
+
const bar = createBar(ctx, opts);
|
|
142
|
+
bars.push(bar);
|
|
143
|
+
render();
|
|
144
|
+
return bar;
|
|
145
|
+
}
|
|
146
|
+
ctx = {
|
|
147
|
+
render,
|
|
148
|
+
_createBar
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
create: (total, opts = {}) => _createBar({ ...opts, total }),
|
|
152
|
+
group,
|
|
153
|
+
stop
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export {
|
|
157
|
+
createMultiBar
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=index.mjs.map
|
package/package.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wukong-progress",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Lightweight ESM CLI progress bar for Node.js with multiple bars, groups, stages and JSON fallback",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"progress",
|
|
8
|
+
"progress-bar",
|
|
9
|
+
"esm",
|
|
10
|
+
"node",
|
|
11
|
+
"terminal",
|
|
12
|
+
"multibar",
|
|
13
|
+
"async",
|
|
14
|
+
"concurrent",
|
|
15
|
+
"cli-tool",
|
|
16
|
+
"json-fallback",
|
|
17
|
+
"wukong"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://tomatobybike.github.io/wukong-progress/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/tomatobybike/wukong-progress"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Tom <tomatobybike@gmail.com>",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.mjs"
|
|
29
|
+
},
|
|
30
|
+
"main": "dist/index.mjs",
|
|
31
|
+
"module": "dist/index.mjs",
|
|
32
|
+
"bin": {
|
|
33
|
+
"wukong-progress": "dist/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin/",
|
|
37
|
+
"src/",
|
|
38
|
+
"README.md",
|
|
39
|
+
"README.zh.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "node scripts/build.js",
|
|
43
|
+
"check:npm:login": "node scripts/check-npm-login.mjs",
|
|
44
|
+
"example": "node examples/index.mjs",
|
|
45
|
+
"format": "prettier --write \"src/**/*.{js,mjs}\"",
|
|
46
|
+
"lint": "eslint src --ext .js,.mjs src",
|
|
47
|
+
"lint:fix": "eslint src --ext .js,.mjs --fix",
|
|
48
|
+
"pack": "npm pack",
|
|
49
|
+
"prepare": "husky install",
|
|
50
|
+
"prepublishOnly": "yarn lint && yarn build",
|
|
51
|
+
"prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
|
|
52
|
+
"push:tags": "git push origin main --follow-tags",
|
|
53
|
+
"prerelease": "yarn lint",
|
|
54
|
+
"release": "yarn check:npm:login && yarn release:patch && npm publish && yarn push:tags",
|
|
55
|
+
"release:major": "yarn lint && standard-version --release-as major",
|
|
56
|
+
"release:minor": "yarn lint && standard-version --release-as minor",
|
|
57
|
+
"release:patch": "yarn lint && standard-version --release-as patch",
|
|
58
|
+
"release:rollback": "git reset --hard HEAD~1 && git tag -d $(git tag --points-at HEAD~1)",
|
|
59
|
+
"report": "wukong-progress report ./profile.json -o my-report.html --open",
|
|
60
|
+
"sort": "sort-package-json",
|
|
61
|
+
"test": "npm run test:node && npm run test:vitest",
|
|
62
|
+
"test:node": "node --test test/node/render.node.test.mjs",
|
|
63
|
+
"test:vitest": "vitest run"
|
|
64
|
+
},
|
|
65
|
+
"husky": {
|
|
66
|
+
"hooks": {
|
|
67
|
+
"pre-commit": "lint-staged"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"lint-staged": {
|
|
71
|
+
"package.json": [
|
|
72
|
+
"sort-package-json"
|
|
73
|
+
],
|
|
74
|
+
"src/**/*.js": [
|
|
75
|
+
"prettier --write",
|
|
76
|
+
"eslint --fix"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"chalk": "^5.3.0"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@trivago/prettier-plugin-sort-imports": "6.0.0",
|
|
84
|
+
"eslint": "8.57.1",
|
|
85
|
+
"eslint-config-airbnb-base": "15.0.0",
|
|
86
|
+
"eslint-config-prettier": "10.1.8",
|
|
87
|
+
"eslint-import-resolver-alias": "1.1.2",
|
|
88
|
+
"eslint-plugin-import": "2.32.0",
|
|
89
|
+
"eslint-plugin-prettier": "5.5.4",
|
|
90
|
+
"eslint-plugin-simple-import-sort": "12.1.1",
|
|
91
|
+
"husky": "9.1.7",
|
|
92
|
+
"lint-staged": "16.2.7",
|
|
93
|
+
"prettier": "3.7.4",
|
|
94
|
+
"prettier-plugin-packagejson": "2.5.20",
|
|
95
|
+
"sort-package-json": "3.6.0",
|
|
96
|
+
"standard-version": "9.5.0",
|
|
97
|
+
"strip-ansi": "^7.1.2",
|
|
98
|
+
"vitest": "^1.3.0"
|
|
99
|
+
},
|
|
100
|
+
"packageManager": "yarn@1.22.22",
|
|
101
|
+
"engines": {
|
|
102
|
+
"node": ">=18"
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/bar.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { color } from './color.mjs'
|
|
2
|
+
import { clamp, formatTime } from './utils.mjs'
|
|
3
|
+
|
|
4
|
+
export function createBar(ctx, opts) {
|
|
5
|
+
const state = {
|
|
6
|
+
total: opts.total,
|
|
7
|
+
current: 0,
|
|
8
|
+
start: Date.now(),
|
|
9
|
+
format: opts.format,
|
|
10
|
+
minWidth: opts.minWidth ?? 10,
|
|
11
|
+
prefix: opts.prefix ?? '',
|
|
12
|
+
hideOnComplete: opts.hideOnComplete ?? false,
|
|
13
|
+
complete: false
|
|
14
|
+
}
|
|
15
|
+
function update(n) {
|
|
16
|
+
state.current = clamp(n, 0, state.total)
|
|
17
|
+
if (state.current >= state.total) {
|
|
18
|
+
state.complete = true
|
|
19
|
+
}
|
|
20
|
+
ctx.render()
|
|
21
|
+
}
|
|
22
|
+
function tick(n = 1) {
|
|
23
|
+
update(state.current + n)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function render(width) {
|
|
27
|
+
const elapsed = (Date.now() - state.start) / 1000
|
|
28
|
+
const percent = state.current / state.total
|
|
29
|
+
|
|
30
|
+
const staticLen = state.format
|
|
31
|
+
.replace(':bar', '')
|
|
32
|
+
.replace(':percent', '100%')
|
|
33
|
+
.replace(':current', state.total)
|
|
34
|
+
.replace(':total', state.total)
|
|
35
|
+
.replace(':eta', '00s').length
|
|
36
|
+
|
|
37
|
+
const barWidth = Math.max(
|
|
38
|
+
state.minWidth,
|
|
39
|
+
width - staticLen - state.prefix.length - 4
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const filled = Math.round(barWidth * percent)
|
|
43
|
+
const bar =
|
|
44
|
+
color.bar('█'.repeat(filled)) + color.dim('░'.repeat(barWidth - filled))
|
|
45
|
+
|
|
46
|
+
const rate = state.current / elapsed
|
|
47
|
+
const eta = rate ? (state.total - state.current) / rate : Infinity
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
(state.prefix ? `${state.prefix} ` : '') +
|
|
51
|
+
state.format
|
|
52
|
+
.replace(':bar', bar)
|
|
53
|
+
.replace(':percent', color.percent(`${Math.round(percent * 100)}%`))
|
|
54
|
+
.replace(':current', state.current)
|
|
55
|
+
.replace(':total', state.total)
|
|
56
|
+
.replace(':elapsed', formatTime(elapsed))
|
|
57
|
+
.replace(':eta', formatTime(eta))
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
state,
|
|
63
|
+
tick,
|
|
64
|
+
update,
|
|
65
|
+
render
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/color.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
let chalk = null
|
|
2
|
+
|
|
3
|
+
try {
|
|
4
|
+
const mod = await import('chalk')
|
|
5
|
+
chalk = mod.default
|
|
6
|
+
} catch { /* empty */ }
|
|
7
|
+
|
|
8
|
+
export const color = {
|
|
9
|
+
bar: s => (chalk ? chalk.cyan(s) : s),
|
|
10
|
+
dim: s => (chalk ? chalk.dim(s) : s),
|
|
11
|
+
percent: s => (chalk ? chalk.green(s) : s),
|
|
12
|
+
}
|
package/src/group.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function createGroup(ctx, name) {
|
|
2
|
+
const bars = []
|
|
3
|
+
|
|
4
|
+
function create(total, opts = {}) {
|
|
5
|
+
const bar = ctx._createBar({
|
|
6
|
+
...opts,
|
|
7
|
+
prefix: opts.prefix ?? name,
|
|
8
|
+
total,
|
|
9
|
+
})
|
|
10
|
+
bars.push(bar)
|
|
11
|
+
return bar
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return { create, bars }
|
|
15
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createMultiBar } from './multibar.mjs'
|
package/src/json.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function renderJSON(stream, bars) {
|
|
2
|
+
const payload = {
|
|
3
|
+
type: 'progress',
|
|
4
|
+
bars: bars.map(b => ({
|
|
5
|
+
name: b.state.prefix,
|
|
6
|
+
current: b.state.current,
|
|
7
|
+
total: b.state.total,
|
|
8
|
+
percent: Math.round(
|
|
9
|
+
(b.state.current / b.state.total) * 100
|
|
10
|
+
),
|
|
11
|
+
complete: b.state.complete,
|
|
12
|
+
})),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
stream.write(`${JSON.stringify(payload) }\n`)
|
|
16
|
+
}
|
package/src/multibar.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createBar } from './bar.mjs'
|
|
2
|
+
import { createGroup } from './group.mjs'
|
|
3
|
+
import { renderJSON } from './json.mjs'
|
|
4
|
+
import { termWidth } from './utils.mjs'
|
|
5
|
+
|
|
6
|
+
export function createMultiBar({
|
|
7
|
+
stream = process.stderr,
|
|
8
|
+
clearOnComplete = false,
|
|
9
|
+
json = false
|
|
10
|
+
} = {}) {
|
|
11
|
+
const bars = []
|
|
12
|
+
let lines = 0
|
|
13
|
+
let active = true
|
|
14
|
+
|
|
15
|
+
const isJSON = json || !stream.isTTY
|
|
16
|
+
|
|
17
|
+
let ctx = null
|
|
18
|
+
function group(name) {
|
|
19
|
+
return createGroup(ctx, name)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function render() {
|
|
23
|
+
if (!active) return
|
|
24
|
+
|
|
25
|
+
if (isJSON) {
|
|
26
|
+
renderJSON(stream, bars)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (lines > 0) {
|
|
31
|
+
stream.write(`\x1b[${lines}A`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const width = termWidth(stream)
|
|
35
|
+
const visible = bars.filter(
|
|
36
|
+
(b) => !(b.state.complete && b.state.hideOnComplete)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const output = visible.map((b) => b.render(width))
|
|
40
|
+
|
|
41
|
+
stream.write(`${output.join('\n')}\n`)
|
|
42
|
+
lines = output.length
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stop() {
|
|
46
|
+
active = false
|
|
47
|
+
if (!isJSON && clearOnComplete) {
|
|
48
|
+
stream.write(`\x1b[${lines}A\x1b[J`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _createBar(opts) {
|
|
53
|
+
const bar = createBar(ctx, opts)
|
|
54
|
+
bars.push(bar)
|
|
55
|
+
render()
|
|
56
|
+
return bar
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ctx = {
|
|
60
|
+
render,
|
|
61
|
+
_createBar
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
create: (total, opts = {}) => _createBar({ ...opts, total }),
|
|
66
|
+
group,
|
|
67
|
+
stop
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/utils.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const clamp = (n, min, max) =>
|
|
2
|
+
Math.min(Math.max(n, min), max)
|
|
3
|
+
|
|
4
|
+
export const formatTime = sec => {
|
|
5
|
+
// eslint-disable-next-line no-restricted-globals
|
|
6
|
+
if (!isFinite(sec)) return '--'
|
|
7
|
+
if (sec < 60) return `${sec.toFixed(1)}s`
|
|
8
|
+
const m = Math.floor(sec / 60)
|
|
9
|
+
const s = Math.floor(sec % 60)
|
|
10
|
+
return `${m}m${s}s`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const termWidth = stream =>
|
|
14
|
+
stream.columns || 80
|